Skip to content

Commit d66b2f9

Browse files
feat: Populate homeAZ on nodeInfo CRD (#4009)
* feat: Populate HomeAZ on nodeInfo CRD * PR comments * Lint
1 parent a0d1a66 commit d66b2f9

File tree

5 files changed

+220
-9
lines changed

5 files changed

+220
-9
lines changed

.pipelines/build/dockerfiles/cns.Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ ENTRYPOINT ["azure-cns.exe"]
1111
EXPOSE 10090
1212

1313
# mcr.microsoft.com/azurelinux/base/core:3.0
14-
FROM --platform=linux/${ARCH} mcr.microsoft.com/azurelinux/base/core@sha256:d472a34802cd535b24ed5fbb7869456e6d8ab2c087faedb9cb32a0efe5b67a15 AS build-helper
14+
FROM --platform=linux/${ARCH} mcr.microsoft.com/azurelinux/base/core@sha256:833693619d523c23b1fe4d9c1f64a6c697e2a82f7a6ee26e1564897c3fe3fa02 AS build-helper
1515
RUN tdnf install -y iptables
1616

1717
# mcr.microsoft.com/azurelinux/distroless/minimal:3.0
18-
FROM --platform=linux/${ARCH} mcr.microsoft.com/azurelinux/distroless/minimal@sha256:77854f8f49c481de03b8c98a5cfba5066616ca5a0213e2f7d443eb542d0f64c4 AS linux
18+
FROM --platform=linux/${ARCH} mcr.microsoft.com/azurelinux/distroless/minimal@sha256:d784c8233e87e8bce2e902ff59a91262635e4cabc25ec55ac0a718344514db3c AS linux
1919
ARG ARTIFACT_DIR .
2020

2121
COPY --from=build-helper /usr/sbin/*tables* /usr/sbin/

cni/Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ ARG OS_VERSION
66
ARG OS
77

88
# mcr.microsoft.com/oss/go/microsoft/golang:1.24-azurelinux3.0
9-
FROM --platform=linux/${ARCH} mcr.microsoft.com/oss/go/microsoft/golang@sha256:6623b87c05c87cf3a213d67f8e917a820227b7fe64582d16deaa8b486a37ef8d AS go
9+
FROM --platform=linux/${ARCH} mcr.microsoft.com/oss/go/microsoft/golang@sha256:281d086598c336073a59d32cd6fc614a892e90c0c0b881e5051d014859739f0e AS go
1010

1111
# mcr.microsoft.com/azurelinux/base/core:3.0
12-
FROM --platform=linux/${ARCH} mcr.microsoft.com/azurelinux/base/core@sha256:d472a34802cd535b24ed5fbb7869456e6d8ab2c087faedb9cb32a0efe5b67a15 AS mariner-core
12+
FROM --platform=linux/${ARCH} mcr.microsoft.com/azurelinux/base/core@sha256:833693619d523c23b1fe4d9c1f64a6c697e2a82f7a6ee26e1564897c3fe3fa02 AS mariner-core
1313

1414
FROM go AS azure-vnet
1515
ARG OS

cns/Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ ARG OS_VERSION
55
ARG OS
66

77
# mcr.microsoft.com/oss/go/microsoft/golang:1.24-azurelinux3.0
8-
FROM --platform=linux/${ARCH} mcr.microsoft.com/oss/go/microsoft/golang@sha256:6623b87c05c87cf3a213d67f8e917a820227b7fe64582d16deaa8b486a37ef8d AS go
8+
FROM --platform=linux/${ARCH} mcr.microsoft.com/oss/go/microsoft/golang@sha256:281d086598c336073a59d32cd6fc614a892e90c0c0b881e5051d014859739f0e AS go
99

1010
# mcr.microsoft.com/azurelinux/base/core:3.0
11-
FROM mcr.microsoft.com/azurelinux/base/core@sha256:d472a34802cd535b24ed5fbb7869456e6d8ab2c087faedb9cb32a0efe5b67a15 AS mariner-core
11+
FROM mcr.microsoft.com/azurelinux/base/core@sha256:833693619d523c23b1fe4d9c1f64a6c697e2a82f7a6ee26e1564897c3fe3fa02 AS mariner-core
1212

1313
# mcr.microsoft.com/azurelinux/distroless/minimal:3.0
14-
FROM mcr.microsoft.com/azurelinux/distroless/minimal@sha256:77854f8f49c481de03b8c98a5cfba5066616ca5a0213e2f7d443eb542d0f64c4 AS mariner-distroless
14+
FROM mcr.microsoft.com/azurelinux/distroless/minimal@sha256:d784c8233e87e8bce2e902ff59a91262635e4cabc25ec55ac0a718344514db3c AS mariner-distroless
1515

1616
FROM --platform=linux/${ARCH} go AS builder
1717
ARG OS

cns/service/main.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ const (
118118
initialIBNICCount = 0
119119
)
120120

121+
// ErrHomeAzNotAvailable indicates that HomeAZ information is not available from CNS
122+
var ErrHomeAzNotAvailable = errors.New("home AZ not available from CNS")
123+
121124
type cniConflistScenario string
122125

123126
const (
@@ -1691,15 +1694,31 @@ func getPodInfoByIPProvider(
16911694
return podInfoByIPProvider, nil
16921695
}
16931696

1694-
// createOrUpdateNodeInfoCRD polls imds to learn the VM Unique ID and then creates or updates the NodeInfo CRD
1695-
// with that vm unique ID
1697+
// createOrUpdateNodeInfoCRD polls IMDS to learn the VM Unique ID and CNS to get the HomeAZ,
1698+
// then creates or updates the NodeInfo CRD with that information
16961699
func createOrUpdateNodeInfoCRD(ctx context.Context, restConfig *rest.Config, node *corev1.Node) error {
16971700
imdsCli := imds.NewClient()
16981701
vmUniqueID, err := imdsCli.GetVMUniqueID(ctx)
16991702
if err != nil {
17001703
return errors.Wrap(err, "error getting vm unique ID from imds")
17011704
}
17021705

1706+
cnsClient, err := cnsclient.New("", cnsReqTimeout)
1707+
if err != nil {
1708+
return errors.Wrap(err, "error creating CNS client")
1709+
}
1710+
var homeAZ string
1711+
homeAzResponse, err := cnsClient.GetHomeAz(ctx)
1712+
if err != nil {
1713+
return errors.Wrap(err, "error getting home AZ from CNS")
1714+
}
1715+
if homeAzResponse.Response.ReturnCode == cnstypes.Success && homeAzResponse.HomeAzResponse.IsSupported {
1716+
homeAZ = fmt.Sprintf("AZ%02d", homeAzResponse.HomeAzResponse.HomeAz)
1717+
} else {
1718+
return errors.Wrapf(ErrHomeAzNotAvailable, "ReturnCode=%d (expected=%d), IsSupported=%t",
1719+
homeAzResponse.Response.ReturnCode, cnstypes.Success, homeAzResponse.HomeAzResponse.IsSupported)
1720+
}
1721+
17031722
directcli, err := client.New(restConfig, client.Options{Scheme: multitenancy.Scheme})
17041723
if err != nil {
17051724
return errors.Wrap(err, "failed to create ctrl client")
@@ -1715,6 +1734,7 @@ func createOrUpdateNodeInfoCRD(ctx context.Context, restConfig *rest.Config, nod
17151734
},
17161735
Spec: mtv1alpha1.NodeInfoSpec{
17171736
VMUniqueID: vmUniqueID,
1737+
HomeAZ: homeAZ,
17181738
},
17191739
}
17201740

cns/service/main_test.go

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,24 @@ package main
33
import (
44
"bytes"
55
"context"
6+
"encoding/json"
7+
"fmt"
68
"io"
79
"net/http"
10+
"net/http/httptest"
11+
"strings"
812
"testing"
13+
"time"
914

1015
"github.com/Azure/azure-container-networking/cns"
1116
"github.com/Azure/azure-container-networking/cns/fakes"
1217
"github.com/Azure/azure-container-networking/cns/logger"
18+
"github.com/Azure/azure-container-networking/crd/multitenancy/api/v1alpha1"
1319
"github.com/stretchr/testify/assert"
20+
"github.com/stretchr/testify/require"
21+
corev1 "k8s.io/api/core/v1"
22+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
23+
"k8s.io/client-go/rest"
1424
)
1525

1626
// MockHTTPClient is a mock implementation of HTTPClient
@@ -69,3 +79,184 @@ func TestSendRegisterNodeRequest_StatusAccepted(t *testing.T) {
6979

7080
assert.Error(t, sendRegisterNodeRequest(ctx, mockClient, httpServiceFake, nodeRegisterReq, url))
7181
}
82+
83+
func TestCreateOrUpdateNodeInfoCRD_PopulatesHomeAZ(t *testing.T) {
84+
vmID := "test-vm-unique-id-12345"
85+
homeAZ := uint(2)
86+
HomeAZStr := fmt.Sprintf("AZ0%d", homeAZ)
87+
88+
// Create mock IMDS server
89+
mockIMDSServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
90+
if strings.Contains(r.URL.Path, "/metadata/instance/compute") {
91+
w.Header().Set("Content-Type", "application/json")
92+
w.WriteHeader(http.StatusOK)
93+
response := map[string]interface{}{
94+
"vmId": vmID,
95+
"name": "test-vm",
96+
"resourceGroupName": "test-rg",
97+
}
98+
_ = json.NewEncoder(w).Encode(response)
99+
return
100+
}
101+
w.WriteHeader(http.StatusNotFound)
102+
}))
103+
defer mockIMDSServer.Close()
104+
105+
// Create mock CNS server
106+
mockCNSServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
107+
if strings.Contains(r.URL.Path, "/homeaz") || strings.Contains(r.URL.Path, "homeaz") {
108+
w.Header().Set("Content-Type", "application/json")
109+
w.WriteHeader(http.StatusOK)
110+
response := map[string]interface{}{
111+
"ReturnCode": 0,
112+
"Message": "",
113+
"HomeAzResponse": map[string]interface{}{
114+
"IsSupported": true,
115+
"HomeAz": homeAZ,
116+
},
117+
}
118+
_ = json.NewEncoder(w).Encode(response)
119+
return
120+
}
121+
w.WriteHeader(http.StatusNotFound)
122+
}))
123+
defer mockCNSServer.Close()
124+
125+
// Set up HTTP transport to mock IMDS and CNS
126+
originalTransport := http.DefaultTransport
127+
defer func() { http.DefaultTransport = originalTransport }()
128+
129+
http.DefaultTransport = &mockTransport{
130+
imdsServer: mockIMDSServer,
131+
cnsServer: mockCNSServer,
132+
original: originalTransport,
133+
}
134+
135+
// Create a mock Kubernetes server that captures the NodeInfo being created
136+
var capturedNodeInfo *v1alpha1.NodeInfo
137+
138+
mockK8sServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
139+
// Handle specific API group discovery - multitenancy.acn.azure.com
140+
if r.URL.Path == "/apis/multitenancy.acn.azure.com/v1alpha1" && r.Method == "GET" {
141+
w.Header().Set("Content-Type", "application/json")
142+
w.WriteHeader(http.StatusOK)
143+
_ = json.NewEncoder(w).Encode(map[string]interface{}{
144+
"kind": "APIResourceList",
145+
"groupVersion": "multitenancy.acn.azure.com/v1alpha1",
146+
"resources": []map[string]interface{}{
147+
{
148+
"name": "nodeinfos",
149+
"singularName": "nodeinfo",
150+
"namespaced": false,
151+
"kind": "NodeInfo",
152+
"verbs": []string{"create", "delete", "get", "list", "patch", "update", "watch"},
153+
},
154+
},
155+
})
156+
return
157+
}
158+
159+
// Handle NodeInfo resource requests
160+
if strings.Contains(r.URL.Path, "nodeinfos") || strings.Contains(r.URL.Path, "multitenancy") {
161+
if r.Method == "POST" || r.Method == "PATCH" || r.Method == "PUT" {
162+
body, _ := io.ReadAll(r.Body)
163+
164+
// Try to parse the NodeInfo from the request
165+
var nodeInfo v1alpha1.NodeInfo
166+
if err := json.Unmarshal(body, &nodeInfo); err == nil {
167+
capturedNodeInfo = &nodeInfo
168+
}
169+
170+
w.Header().Set("Content-Type", "application/json")
171+
w.WriteHeader(http.StatusOK)
172+
// Return the created NodeInfo
173+
_ = json.NewEncoder(w).Encode(map[string]interface{}{
174+
"apiVersion": "multitenancy.acn.azure.com/v1alpha1",
175+
"kind": "NodeInfo",
176+
"metadata": map[string]interface{}{
177+
"name": "test-node",
178+
},
179+
"spec": map[string]interface{}{
180+
"vmUniqueID": vmID,
181+
"homeAZ": HomeAZStr,
182+
},
183+
})
184+
return
185+
}
186+
}
187+
188+
// Default success response for any other API calls
189+
w.Header().Set("Content-Type", "application/json")
190+
w.WriteHeader(http.StatusOK)
191+
_ = json.NewEncoder(w).Encode(map[string]interface{}{
192+
"kind": "Status",
193+
"status": "Success",
194+
})
195+
}))
196+
defer mockK8sServer.Close()
197+
198+
// Test the function with mocked dependencies
199+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
200+
defer cancel()
201+
202+
// Point to our mock Kubernetes server
203+
restConfig := &rest.Config{
204+
Host: mockK8sServer.URL,
205+
}
206+
207+
node := &corev1.Node{
208+
ObjectMeta: metav1.ObjectMeta{Name: "test-node"},
209+
}
210+
211+
// Call the createOrUpdateNodeInfoCRD function
212+
err := createOrUpdateNodeInfoCRD(ctx, restConfig, node)
213+
214+
// Verify the function succeeded
215+
require.NoError(t, err, "Function should succeed with mocked dependencies")
216+
217+
// Verify the captured values
218+
assert.NotNil(t, capturedNodeInfo, "NodeInfo should have been captured from K8s API call")
219+
if capturedNodeInfo != nil {
220+
assert.Equal(t, vmID, capturedNodeInfo.Spec.VMUniqueID, "VMUniqueID should be from IMDS")
221+
assert.Equal(t, HomeAZStr, capturedNodeInfo.Spec.HomeAZ, "HomeAZ should be formatted from CNS response")
222+
}
223+
}
224+
225+
// mockTransport redirects HTTP requests to mock servers for testing.
226+
// It intercepts requests to IMDS and CNS endpoints and routes them to local test servers.
227+
type mockTransport struct {
228+
imdsServer *httptest.Server
229+
cnsServer *httptest.Server
230+
original http.RoundTripper
231+
}
232+
233+
func (m *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
234+
// Redirect IMDS calls to mock IMDS server
235+
if req.URL.Host == "169.254.169.254" {
236+
req.URL.Scheme = "http"
237+
req.URL.Host = strings.TrimPrefix(m.imdsServer.URL, "http://")
238+
resp, err := m.original.RoundTrip(req)
239+
if err != nil {
240+
return nil, fmt.Errorf("IMDS mock transport failed: %w", err)
241+
}
242+
return resp, nil
243+
}
244+
245+
// Redirect CNS calls to mock CNS server
246+
if req.URL.Host == "localhost:10090" || strings.Contains(req.URL.Host, "10090") {
247+
req.URL.Scheme = "http"
248+
req.URL.Host = strings.TrimPrefix(m.cnsServer.URL, "http://")
249+
resp, err := m.original.RoundTrip(req)
250+
if err != nil {
251+
return nil, fmt.Errorf("CNS mock transport failed: %w", err)
252+
}
253+
return resp, nil
254+
}
255+
256+
// All other calls go through original transport
257+
resp, err := m.original.RoundTrip(req)
258+
if err != nil {
259+
return nil, fmt.Errorf("mock transport failed: %w", err)
260+
}
261+
return resp, nil
262+
}

0 commit comments

Comments
 (0)