Repository: digitalocean/doctl
Pull request #1663 (2025-02-19 08:28:11)

[VPC-3917] Support PIA get service key operation
Testing:

```
➜  builds git:(VPC-3917/get_sk) ✗ ./doctl --context new_s2 network --type "partner" interconnect-attachment get-service-key 7b48cbc3-cacb-418e-908c-a9a86189a642
Value                                   State      CreatedAt
8f6d51fe-XXXXXXXXXX   CREATED    2025-02-17 15:07:29 +0000 UTC
```


Patch:
diff --git a/commands/displayers/partner_interconnect_attachment.go b/commands/displayers/partner_interconnect_attachment.go
index ccd9b9cc..e44c5da3 100644
--- a/commands/displayers/partner_interconnect_attachment.go
+++ b/commands/displayers/partner_interconnect_attachment.go
@@ -166,3 +166,42 @@ func (v *PartnerInterconnectAttachmentBgpAuthKey) KV() []map[string]any {
 	out = append(out, o)
 	return out
 }
+
+type PartnerInterconnectAttachmentServiceKey struct {
+	Key do.PartnerInterconnectAttachmentServiceKey
+}
+
+var _ Displayable = &PartnerInterconnectAttachmentServiceKey{}
+
+func (v *PartnerInterconnectAttachmentServiceKey) JSON(out io.Writer) error {
+	return writeJSON(v.Key, out)
+}
+
+func (v *PartnerInterconnectAttachmentServiceKey) Cols() []string {
+	return []string{
+		"Value",
+		"State",
+		"CreatedAt",
+	}
+}
+
+func (v *PartnerInterconnectAttachmentServiceKey) ColMap() map[string]string {
+	return map[string]string{
+		"Value":     "Value",
+		"State":     "State",
+		"CreatedAt": "CreatedAt",
+	}
+}
+
+func (v *PartnerInterconnectAttachmentServiceKey) KV() []map[string]any {
+	out := make([]map[string]any, 0, 1)
+
+	o := map[string]any{
+		"Value":     v.Key.ServiceKey.Value,
+		"State":     v.Key.ServiceKey.State,
+		"CreatedAt": v.Key.ServiceKey.CreatedAt,
+	}
+	out = append(out, o)
+
+	return out
+}
diff --git a/commands/partner_interconnect_attachment.go b/commands/partner_interconnect_attachment.go
index b9b393e1..4cd9b47e 100644
--- a/commands/partner_interconnect_attachment.go
+++ b/commands/partner_interconnect_attachment.go
@@ -146,6 +146,18 @@ vpc-ids "270a76ed-1bb7-4c5d-a6a5-e863de086940"`
 	cmdGetPartnerIAGetBGPAuthKey.Example = `The following example retrieves information about a Service key of Partner Interconnect Attachment with the ID ` + "`" + `f81d4fae-7dec-11d0-a765-00a0c91e6bf6` + "`" +
 		`: doctl network --type "partner" interconnect-attachment get-bgp-auth-key f81d4fae-7dec-11d0-a765-00a0c91e6bf6`
 
+	interconnectAttachmentServiceKeyDetails := `
+- The Service key Value
+- The Service key State
+- The Service key CreatedAt`
+
+	cmdGetPartnerIAServiceKey := CmdBuilder(cmd, RunGetPartnerInterconnectAttachmentServiceKey, "get-service-key <interconnect-attachment-id>",
+		"Retrieves a Service key of Partner Interconnect Attachment", "Retrieves information about a Service key of Partner Interconnect Attachment, including:"+interconnectAttachmentServiceKeyDetails, Writer,
+		aliasOpt("g-service-key"), displayerType(&displayers.PartnerInterconnectAttachmentServiceKey{}))
+	AddStringFlag(cmdGetPartnerIAServiceKey, doctl.ArgInterconnectAttachmentType, "", "partner", "Specify interconnect attachment type (e.g., partner)")
+	cmdGetPartnerIAServiceKey.Example = `The following example retrieves information about a Service key of Partner Interconnect Attachment with the ID ` + "`" + `f81d4fae-7dec-11d0-a765-00a0c91e6bf6` + "`" +
+		`: doctl network --type "partner" interconnect-attachment get-service-key f81d4fae-7dec-11d0-a765-00a0c91e6bf6`
+
 	return cmd
 }
 
@@ -366,6 +378,31 @@ func RunGetPartnerInterconnectAttachmentBGPAuthKey(c *CmdConfig) error {
 	return c.Display(item)
 }
 
+// RunGetPartnerInterconnectAttachmentServiceKey retrieves service key of existing Partner Interconnect Attachment
+func RunGetPartnerInterconnectAttachmentServiceKey(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()
+	serviceKey, err := pias.GetServiceKey(iaID)
+	if err != nil {
+		return err
+	}
+
+	item := &displayers.PartnerInterconnectAttachmentServiceKey{
+		Key: *serviceKey,
+	}
+	return c.Display(item)
+}
+
 // RunPartnerInterconnectAttachmentDelete deletes an existing Partner Interconnect Attachment by its identifier.
 func RunPartnerInterconnectAttachmentDelete(c *CmdConfig) error {
 
diff --git a/do/mocks/PartnerInterconnectAttachmentsService.go b/do/mocks/PartnerInterconnectAttachmentsService.go
index ee717d86..7ae3c60c 100644
--- a/do/mocks/PartnerInterconnectAttachmentsService.go
+++ b/do/mocks/PartnerInterconnectAttachmentsService.go
@@ -100,6 +100,21 @@ func (mr *MockPartnerInterconnectAttachmentsServiceMockRecorder) GetPartnerInter
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPartnerInterconnectAttachment", reflect.TypeOf((*MockPartnerInterconnectAttachmentsService)(nil).GetPartnerInterconnectAttachment), iaID)
 }
 
+// GetServiceKey mocks base method.
+func (m *MockPartnerInterconnectAttachmentsService) GetServiceKey(iaID string) (*do.PartnerInterconnectAttachmentServiceKey, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetServiceKey", iaID)
+	ret0, _ := ret[0].(*do.PartnerInterconnectAttachmentServiceKey)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetServiceKey indicates an expected call of GetServiceKey.
+func (mr *MockPartnerInterconnectAttachmentsServiceMockRecorder) GetServiceKey(iaID any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServiceKey", reflect.TypeOf((*MockPartnerInterconnectAttachmentsService)(nil).GetServiceKey), iaID)
+}
+
 // ListPartnerInterconnectAttachmentRoutes mocks base method.
 func (m *MockPartnerInterconnectAttachmentsService) ListPartnerInterconnectAttachmentRoutes(iaID string) (do.PartnerInterconnectAttachmentRoutes, error) {
 	m.ctrl.T.Helper()
diff --git a/do/partner_interconnect_attachments.go b/do/partner_interconnect_attachments.go
index 76fc8e0e..b1b5fab6 100644
--- a/do/partner_interconnect_attachments.go
+++ b/do/partner_interconnect_attachments.go
@@ -45,6 +45,11 @@ type PartnerInterconnectAttachmentRegenerateServiceKey struct {
 	*godo.RegenerateServiceKey
 }
 
+// PartnerInterconnectAttachmentServiceKey wraps a godo ServiceKey.
+type PartnerInterconnectAttachmentServiceKey struct {
+	*godo.ServiceKey
+}
+
 // PartnerInterconnectAttachmentsService is an interface for interacting with
 // DigitalOcean's partner interconnect attachments api.
 type PartnerInterconnectAttachmentsService interface {
@@ -56,6 +61,7 @@ type PartnerInterconnectAttachmentsService interface {
 	ListPartnerInterconnectAttachmentRoutes(iaID string) (PartnerInterconnectAttachmentRoutes, error)
 	GetBGPAuthKey(iaID string) (*PartnerInterconnectAttachmentBGPAuthKey, error)
 	RegenerateServiceKey(iaID string) (*PartnerInterconnectAttachmentRegenerateServiceKey, error)
+	GetServiceKey(iaID string) (*PartnerInterconnectAttachmentServiceKey, error)
 }
 
 var _ PartnerInterconnectAttachmentsService = &partnerInterconnectAttachmentsService{}
@@ -178,3 +184,12 @@ func (p *partnerInterconnectAttachmentsService) RegenerateServiceKey(iaID string
 	}
 	return &PartnerInterconnectAttachmentRegenerateServiceKey{RegenerateServiceKey: regenerateServiceKey}, nil
 }
+
+// GetServiceKey retrieves a service key of a partner interconnect attachment.
+func (p *partnerInterconnectAttachmentsService) GetServiceKey(iaID string) (*PartnerInterconnectAttachmentServiceKey, error) {
+	serviceKey, _, err := p.client.PartnerInterconnectAttachments.GetServiceKey(context.TODO(), iaID)
+	if err != nil {
+		return nil, err
+	}
+	return &PartnerInterconnectAttachmentServiceKey{ServiceKey: serviceKey}, nil
+}


Test patch:
diff --git a/commands/partner_interconnect_attachment_test.go b/commands/partner_interconnect_attachment_test.go
index 5d8e4a1f..c38421a6 100644
--- a/commands/partner_interconnect_attachment_test.go
+++ b/commands/partner_interconnect_attachment_test.go
@@ -50,13 +50,21 @@ var (
 			Value: "test-bgp-auth-key",
 		},
 	}
+
+	testServiceKey = do.PartnerInterconnectAttachmentServiceKey{
+		ServiceKey: &godo.ServiceKey{
+			Value:     "test-service-key",
+			State:     "active",
+			CreatedAt: time.Date(2025, 1, 30, 0, 0, 0, 0, time.UTC),
+		},
+	}
 )
 
 func TestPartnerInterconnectAttachmentsCommand(t *testing.T) {
 	cmd := PartnerInterconnectAttachments()
 	assert.NotNil(t, cmd)
 
-	assertCommandNames(t, cmd, "create", "get", "list", "delete", "update", "list-routes", "regenerate-service-key", "get-bgp-auth-key")
+	assertCommandNames(t, cmd, "create", "get", "list", "delete", "update", "list-routes", "regenerate-service-key", "get-bgp-auth-key", "get-service-key")
 }
 
 func TestPartnerInterconnectAttachmentCreate(t *testing.T) {
@@ -213,3 +221,17 @@ func TestInterconnectAttachmentsBgpAuthKey(t *testing.T) {
 		assert.NoError(t, err)
 	})
 }
+
+func TestInterconnectAttachmentsGetServiceKey(t *testing.T) {
+	withTestClient(t, func(config *CmdConfig, tm *tcMocks) {
+		config.Doit.Set(config.NS, doctl.ArgInterconnectAttachmentType, "partner")
+
+		iaID := "e819b321-a9a1-4078-b437-8e6b8bf13530"
+		tm.partnerInterconnectAttachment.EXPECT().GetServiceKey(iaID).Return(&testServiceKey, nil)
+
+		config.Args = append(config.Args, iaID)
+
+		err := RunGetPartnerInterconnectAttachmentServiceKey(config)
+		assert.NoError(t, err)
+	})
+}
diff --git a/integration/partner_interconnect_attachment_test.go b/integration/partner_interconnect_attachment_test.go
index 50e74440..72521592 100644
--- a/integration/partner_interconnect_attachment_test.go
+++ b/integration/partner_interconnect_attachment_test.go
@@ -81,6 +81,19 @@ test-bgp-auth-key
 	"bgp_auth_key": {
 		"value": "test-bgp-auth-key"
 	}
+}`
+	interconnectGetServiceKeyOutput = `
+Value               State     CreatedAt
+test-service-key    active    2025-01-30 12:00:00 +0000 UTC	
+	`
+
+	interconnectGetServiceKeyResponse = `
+{
+	"service_key": {
+		"created_at": "2025-01-30T12:00:00Z",
+		"value": "test-service-key",
+		"state": "active"
+	}
 }`
 )
 
@@ -355,3 +368,75 @@ var _ = suite("partner_interconnect_attachments/get-bgp-auth-key", func(t *testi
 		})
 	})
 })
+
+var _ = suite("partner_interconnect_attachments/get-service-key", 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/service_key":
+				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(interconnectGetServiceKeyResponse))
+			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 service key", func() {
+			cmd := exec.Command(builtBinaryPath,
+				"-t", "some-magic-token",
+				"-u", server.URL,
+				"network",
+				"interconnect-attachment",
+				"get-service-key",
+				"c5537207-ebf0-47cb-bc10-6fac717cd672",
+			)
+
+			output, err := cmd.CombinedOutput()
+			expect.NoError(err, fmt.Sprintf("received error output: %s", output))
+			expect.Equal(strings.TrimSpace(interconnectGetServiceKeyOutput), strings.TrimSpace(string(output)))
+		})
+	})
+
+	when("format and no-header flags are passed", func() {
+		it("gets the specified service key", func() {
+			cmd := exec.Command(builtBinaryPath,
+				"-t", "some-magic-token",
+				"-u", server.URL,
+				"network",
+				"interconnect-attachment",
+				"get-service-key",
+				"--format", "Value",
+				"--no-header",
+				"c5537207-ebf0-47cb-bc10-6fac717cd672",
+			)
+
+			output, err := cmd.CombinedOutput()
+			expect.NoError(err, fmt.Sprintf("received error output: %s", output))
+			expect.Equal("test-service-key", strings.TrimSpace(string(output)))
+		})
+	})
+})