Skip to content

Commit 6c50d0d

Browse files
authored
feat: Add CNS API to retrieve VMUniqueID from IMDS (#2842)
* Add CNS API to retrieve VMUniqueID from IMDS * Address the PR review comments * Address the security comment from Evans to expose this API wherever needed * fixed the linter error * address the PR comments from Matt * lowercase the struct json fields
1 parent 0d29472 commit 6c50d0d

File tree

13 files changed

+173
-13
lines changed

13 files changed

+173
-13
lines changed

cns/NetworkContainerContract.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
const (
1919
SetOrchestratorType = "/network/setorchestratortype"
2020
GetHomeAz = "/homeaz"
21+
GetVMUniqueID = "/metadata/vmuniqueid"
2122
CreateOrUpdateNetworkContainer = "/network/createorupdatenetworkcontainer"
2223
DeleteNetworkContainer = "/network/deletenetworkcontainer"
2324
PublishNetworkContainer = "/network/publishnetworkcontainer"

cns/api.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,8 +306,8 @@ type IpamPoolMonitorStateSnapshot struct {
306306

307307
// Response describes generic response from CNS.
308308
type Response struct {
309-
ReturnCode types.ResponseCode
310-
Message string
309+
ReturnCode types.ResponseCode `json:"ReturnCode"`
310+
Message string `json:"Message"`
311311
}
312312

313313
// NumOfCPUCoresResponse describes num of cpu cores present on host.
@@ -373,3 +373,8 @@ type EndpointRequest struct {
373373
HostVethName string `json:"hostVethName"`
374374
InterfaceName string `json:"InterfaceName"`
375375
}
376+
377+
type GetVMUniqueIDResponse struct {
378+
Response Response `json:"response"`
379+
VMUniqueID string `json:"vmuniqueid"`
380+
}

cns/client/client_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,9 @@ func TestMain(m *testing.M) {
151151
logger.InitLogger(logName, 0, 0, tmpLogDir+"/")
152152
config := common.ServiceConfig{}
153153

154-
httpRestService, err := restserver.NewHTTPRestService(&config, &fakes.WireserverClientFake{}, &fakes.WireserverProxyFake{}, &fakes.NMAgentClientFake{}, nil, nil, nil)
154+
httpRestService, err := restserver.NewHTTPRestService(&config, &fakes.WireserverClientFake{},
155+
&fakes.WireserverProxyFake{}, &fakes.NMAgentClientFake{}, nil, nil, nil,
156+
fakes.NewMockIMDSClient())
155157
svc = httpRestService
156158
httpRestService.Name = "cns-test-server"
157159

cns/fakes/imdsclientfake.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,21 @@ package fakes
99
import (
1010
"context"
1111

12+
"github.com/Azure/azure-container-networking/cns/imds"
1213
"github.com/Azure/azure-container-networking/cns/wireserver"
1314
)
1415

1516
const (
1617
// HostPrimaryIP 10.0.0.4
1718
HostPrimaryIP = "10.0.0.4"
1819
// HostSubnet 10.0.0.0/24
19-
HostSubnet = "10.0.0.0/24"
20+
HostSubnet = "10.0.0.0/24"
21+
SimulateError MockIMDSCtxKey = "simulate-error"
2022
)
2123

2224
type WireserverClientFake struct{}
25+
type MockIMDSCtxKey string
26+
type MockIMDSClient struct{}
2327

2428
func (c *WireserverClientFake) GetInterfaces(ctx context.Context) (*wireserver.GetInterfacesResult, error) {
2529
return &wireserver.GetInterfacesResult{
@@ -41,3 +45,15 @@ func (c *WireserverClientFake) GetInterfaces(ctx context.Context) (*wireserver.G
4145
},
4246
}, nil
4347
}
48+
49+
func NewMockIMDSClient() *MockIMDSClient {
50+
return &MockIMDSClient{}
51+
}
52+
53+
func (m *MockIMDSClient) GetVMUniqueID(ctx context.Context) (string, error) {
54+
if ctx.Value(SimulateError) != nil {
55+
return "", imds.ErrUnexpectedStatusCode
56+
}
57+
58+
return "55b8499d-9b42-4f85-843f-24ff69f4a643", nil
59+
}

cns/imds/client_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,29 @@ func TestIMDSInvalidJSON(t *testing.T) {
7474
_, err := imdsClient.GetVMUniqueID(context.Background())
7575
require.Error(t, err, "expected json decoding error")
7676
}
77+
78+
func TestInvalidVMUniqueID(t *testing.T) {
79+
computeMetadata, err := os.ReadFile("testdata/invalidComputeMetadata.json")
80+
require.NoError(t, err, "error reading testdata compute metadata file")
81+
82+
mockIMDSServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
83+
// request header "Metadata: true" must be present
84+
metadataHeader := r.Header.Get("Metadata")
85+
assert.Equal(t, "true", metadataHeader)
86+
87+
// query params should include apiversion and json format
88+
apiVersion := r.URL.Query().Get("api-version")
89+
assert.Equal(t, "2021-01-01", apiVersion)
90+
format := r.URL.Query().Get("format")
91+
assert.Equal(t, "json", format)
92+
w.WriteHeader(http.StatusOK)
93+
_, writeErr := w.Write(computeMetadata)
94+
require.NoError(t, writeErr, "error writing response")
95+
}))
96+
defer mockIMDSServer.Close()
97+
98+
imdsClient := imds.NewClient(imds.Endpoint(mockIMDSServer.URL))
99+
vmUniqueID, err := imdsClient.GetVMUniqueID(context.Background())
100+
require.Error(t, err, "error querying testserver")
101+
require.Equal(t, "", vmUniqueID)
102+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"azEnvironment": "AzurePublicCloud",
3+
"location": "westus2",
4+
"vmId": ""
5+
}

cns/restserver/api.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1522,3 +1522,42 @@ func (service *HTTPRestService) nmAgentSupportedApisHandler(w http.ResponseWrite
15221522

15231523
logger.Response(service.Name, nmAgentSupportedApisResponse, resp.ReturnCode, serviceErr)
15241524
}
1525+
1526+
// getVMUniqueID retrieves VMUniqueID from the IMDS
1527+
func (service *HTTPRestService) getVMUniqueID(w http.ResponseWriter, r *http.Request) {
1528+
logger.Request(service.Name, "getVMUniqueID", nil)
1529+
ctx := r.Context()
1530+
1531+
switch r.Method {
1532+
case http.MethodGet:
1533+
vmUniqueID, err := service.imdsClient.GetVMUniqueID(ctx)
1534+
if err != nil {
1535+
resp := cns.GetVMUniqueIDResponse{
1536+
Response: cns.Response{
1537+
ReturnCode: types.UnexpectedError,
1538+
Message: errors.Wrap(err, "failed to get vmuniqueid").Error(),
1539+
},
1540+
}
1541+
respondJSON(w, http.StatusInternalServerError, resp)
1542+
logger.Response(service.Name, resp, resp.Response.ReturnCode, err)
1543+
return
1544+
}
1545+
1546+
resp := cns.GetVMUniqueIDResponse{
1547+
Response: cns.Response{
1548+
ReturnCode: types.Success,
1549+
},
1550+
VMUniqueID: vmUniqueID,
1551+
}
1552+
respondJSON(w, http.StatusOK, resp)
1553+
logger.Response(service.Name, resp, resp.Response.ReturnCode, err)
1554+
1555+
default:
1556+
returnMessage := fmt.Sprintf("[Azure CNS] Error. getVMUniqueID did not receive a GET."+
1557+
" Received: %s", r.Method)
1558+
returnCode := types.UnsupportedVerb
1559+
service.setResponse(w, returnCode, cns.GetHomeAzResponse{
1560+
Response: cns.Response{ReturnCode: returnCode, Message: returnMessage},
1561+
})
1562+
}
1563+
}

cns/restserver/api_test.go

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020

2121
"github.com/Azure/azure-container-networking/cns"
2222
"github.com/Azure/azure-container-networking/cns/common"
23+
"github.com/Azure/azure-container-networking/cns/configuration"
2324
"github.com/Azure/azure-container-networking/cns/fakes"
2425
"github.com/Azure/azure-container-networking/cns/logger"
2526
"github.com/Azure/azure-container-networking/cns/types"
@@ -29,6 +30,7 @@ import (
2930
"github.com/Azure/azure-container-networking/store"
3031
"github.com/pkg/errors"
3132
"github.com/stretchr/testify/assert"
33+
"github.com/stretchr/testify/require"
3234
)
3335

3436
const (
@@ -172,8 +174,10 @@ func TestMain(m *testing.M) {
172174
var err error
173175
logger.InitLogger("testlogs", 0, 0, "./")
174176

175-
// Create the service.
176-
if err = startService(); err != nil {
177+
// Create the service. If CRD channel mode is needed, then at the start of the test,
178+
// it can stop the service (service.Stop), invoke startService again with new ServiceConfig (with CRD mode)
179+
// perform the test and then restore the service again.
180+
if err = startService(common.ServiceConfig{ChannelMode: cns.Direct}, configuration.CNSConfig{}); err != nil {
177181
fmt.Printf("Failed to start CNS Service. Error: %v", err)
178182
os.Exit(1)
179183
}
@@ -1666,9 +1670,9 @@ func setEnv(t *testing.T) *httptest.ResponseRecorder {
16661670
return w
16671671
}
16681672

1669-
func startService() error {
1673+
func startService(serviceConfig common.ServiceConfig, _ configuration.CNSConfig) error {
16701674
// Create the service.
1671-
config := common.ServiceConfig{}
1675+
config := serviceConfig
16721676

16731677
// Create the key value fileStore.
16741678
fileStore, err := store.NewJsonFileStore(cnsJsonFileName, processlock.NewMockFileLock(false), nil)
@@ -1679,7 +1683,8 @@ func startService() error {
16791683
config.Store = fileStore
16801684

16811685
nmagentClient := &fakes.NMAgentClientFake{}
1682-
service, err = NewHTTPRestService(&config, &fakes.WireserverClientFake{}, &fakes.WireserverProxyFake{}, nmagentClient, nil, nil, nil)
1686+
service, err = NewHTTPRestService(&config, &fakes.WireserverClientFake{}, &fakes.WireserverProxyFake{},
1687+
nmagentClient, nil, nil, nil, fakes.NewMockIMDSClient())
16831688
if err != nil {
16841689
return err
16851690
}
@@ -1758,6 +1763,43 @@ func contains(networkContainers []cns.GetNetworkContainerResponse, str string) b
17581763
return false
17591764
}
17601765

1766+
// Testing GetVMUniqueID API handler with success
1767+
func TestGetVMUniqueIDSuccess(t *testing.T) {
1768+
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, cns.GetVMUniqueID, http.NoBody)
1769+
if err != nil {
1770+
t.Fatal(err)
1771+
}
1772+
1773+
w := httptest.NewRecorder()
1774+
mux.ServeHTTP(w, req)
1775+
1776+
var vmIDResp cns.GetVMUniqueIDResponse
1777+
err = decodeResponse(w, &vmIDResp)
1778+
require.NoError(t, err)
1779+
assert.Equal(t, types.Success, vmIDResp.Response.ReturnCode)
1780+
assert.Equal(t, "55b8499d-9b42-4f85-843f-24ff69f4a643", vmIDResp.VMUniqueID)
1781+
}
1782+
1783+
// Testing GetVMUniqueID API handler with failure
1784+
func TestGetVMUniqueIDFailed(t *testing.T) {
1785+
ctx := context.TODO()
1786+
ctx = context.WithValue(ctx, fakes.SimulateError, Interface{})
1787+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, cns.GetVMUniqueID, http.NoBody)
1788+
if err != nil {
1789+
t.Fatal(err)
1790+
}
1791+
1792+
w := httptest.NewRecorder()
1793+
mux.ServeHTTP(w, req)
1794+
1795+
assert.Equal(t, http.StatusInternalServerError, w.Code)
1796+
1797+
var vmIDResp cns.GetVMUniqueIDResponse
1798+
err = json.NewDecoder(w.Body).Decode(&vmIDResp)
1799+
require.NoError(t, err)
1800+
assert.Equal(t, types.UnexpectedError, vmIDResp.Response.ReturnCode)
1801+
}
1802+
17611803
// IGNORE TEST AS IT IS FAILING. TODO:- Fix it https://msazure.visualstudio.com/One/_workitems/edit/7720083
17621804
// // Tests CreateNetwork functionality.
17631805

cns/restserver/internalapi_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import (
1616
"time"
1717

1818
"github.com/Azure/azure-container-networking/cns"
19+
"github.com/Azure/azure-container-networking/cns/common"
20+
"github.com/Azure/azure-container-networking/cns/configuration"
1921
"github.com/Azure/azure-container-networking/cns/fakes"
2022
"github.com/Azure/azure-container-networking/cns/types"
2123
"github.com/Azure/azure-container-networking/crd/nodenetworkconfig/api/v1alpha"
@@ -1056,7 +1058,7 @@ func restartService() {
10561058
fmt.Println("Restart Service")
10571059

10581060
service.Stop()
1059-
if err := startService(); err != nil {
1061+
if err := startService(common.ServiceConfig{}, configuration.CNSConfig{}); err != nil {
10601062
fmt.Printf("Failed to restart CNS Service. Error: %v", err)
10611063
os.Exit(1)
10621064
}

cns/restserver/ipam_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,9 @@ type ncState struct {
7272

7373
func getTestService() *HTTPRestService {
7474
var config common.ServiceConfig
75-
httpsvc, _ := NewHTTPRestService(&config, &fakes.WireserverClientFake{}, &fakes.WireserverProxyFake{}, &fakes.NMAgentClientFake{}, store.NewMockStore(""), nil, nil)
75+
httpsvc, _ := NewHTTPRestService(&config, &fakes.WireserverClientFake{}, &fakes.WireserverProxyFake{},
76+
&fakes.NMAgentClientFake{}, store.NewMockStore(""), nil, nil,
77+
fakes.NewMockIMDSClient())
7678
svc = httpsvc
7779
setOrchestratorTypeInternal(cns.KubernetesCRD)
7880

0 commit comments

Comments
 (0)