Skip to content

Commit 44baa98

Browse files
jamandbreuerfelix
andauthored
feat: Add InternalIPs to CreateMachineResponse (#140)
* fix: add missing client test suite and fix test case * feat: Add InternalIPs to CreateMachineResponse * chore: Refactor NIC fetching in CreateMachine (PR feedback) Co-authored-by: Felix Breuer <f.breuer94@gmail.com> --------- Co-authored-by: Felix Breuer <f.breuer94@gmail.com>
1 parent cdb757b commit 44baa98

File tree

7 files changed

+158
-21
lines changed

7 files changed

+158
-21
lines changed

pkg/client/client_suite_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package client
2+
3+
import (
4+
"testing"
5+
6+
. "github.com/onsi/ginkgo/v2"
7+
. "github.com/onsi/gomega"
8+
)
9+
10+
func TestClient(t *testing.T) {
11+
RegisterFailHandler(Fail)
12+
RunSpecs(t, "Client Suite")
13+
}

pkg/client/mock/client.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ func (m *StackitClient) GetNICsForServer(ctx context.Context, projectID, region,
5959
if m.GetNICsFunc != nil {
6060
return m.GetNICsFunc(ctx, projectID, region, serverID)
6161
}
62-
return []*client.NIC{}, nil
62+
return []*client.NIC{
63+
{ID: "default-nic-id", NetworkID: "default-network-id"},
64+
}, nil
6365
}
6466

6567
func (m *StackitClient) UpdateNIC(ctx context.Context, projectID, region, networkID, nicID string, allowedAddresses []string) (*client.NIC, error) {

pkg/client/sdk.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,8 @@ func convertSDKNICtoNIC(nic *iaas.NIC) *NIC {
332332
ID: nic.GetId(),
333333
NetworkID: nic.GetNetworkId(),
334334
AllowedAddresses: addresses,
335+
IPv4: nic.GetIpv4(),
336+
IPv6: nic.GetIpv6(),
335337
}
336338
}
337339

pkg/client/sdk_test.go

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
. "github.com/onsi/ginkgo/v2"
99
. "github.com/onsi/gomega"
1010
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
11+
iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api"
1112
)
1213

1314
var _ = Describe("SDK Client Helpers", func() {
@@ -122,7 +123,7 @@ var _ = Describe("SDK Type Conversion Helpers", func() {
122123
result := convertLabelsToSDK(labels)
123124

124125
Expect(result).NotTo(BeNil())
125-
Expect(result).To(HaveLen(2))
126+
Expect(result).To(HaveLen(1))
126127
Expect(result["kubernetes.io/machine"]).To(Equal("test-machine"))
127128
})
128129
})
@@ -203,6 +204,64 @@ var _ = Describe("SDK Type Conversion Helpers", func() {
203204
})
204205
})
205206

207+
Describe("convertSDKNICtoNIC", func() {
208+
It("should populate IPv4 and IPv6 from SDK NIC", func() {
209+
sdkNIC := &iaas.NIC{
210+
Id: new("nic-1"),
211+
NetworkId: new("net-1"),
212+
Ipv4: new("10.0.0.5"),
213+
Ipv6: new("fd00::1"),
214+
}
215+
216+
result := convertSDKNICtoNIC(sdkNIC)
217+
218+
Expect(result.ID).To(Equal("nic-1"))
219+
Expect(result.NetworkID).To(Equal("net-1"))
220+
Expect(result.IPv4).To(Equal("10.0.0.5"))
221+
Expect(result.IPv6).To(Equal("fd00::1"))
222+
})
223+
224+
It("should handle a NIC with only IPv4", func() {
225+
sdkNIC := &iaas.NIC{
226+
Id: new("nic-1"),
227+
NetworkId: new("net-1"),
228+
Ipv4: new("10.0.0.5"),
229+
}
230+
231+
result := convertSDKNICtoNIC(sdkNIC)
232+
233+
Expect(result.IPv4).To(Equal("10.0.0.5"))
234+
Expect(result.IPv6).To(BeEmpty())
235+
})
236+
237+
It("should handle a NIC with neither IPv4 nor IPv6", func() {
238+
sdkNIC := &iaas.NIC{
239+
Id: new("nic-1"),
240+
NetworkId: new("net-1"),
241+
}
242+
243+
result := convertSDKNICtoNIC(sdkNIC)
244+
245+
Expect(result.IPv4).To(BeEmpty())
246+
Expect(result.IPv6).To(BeEmpty())
247+
})
248+
249+
It("should populate AllowedAddresses", func() {
250+
addr := "10.0.0.0/8"
251+
sdkNIC := &iaas.NIC{
252+
Id: new("nic-1"),
253+
NetworkId: new("net-1"),
254+
AllowedAddresses: []iaas.AllowedAddressesInner{
255+
{String: &addr},
256+
},
257+
}
258+
259+
result := convertSDKNICtoNIC(sdkNIC)
260+
261+
Expect(result.AllowedAddresses).To(ConsistOf("10.0.0.0/8"))
262+
})
263+
})
264+
206265
Describe("NewStackitClient", func() {
207266
Context("with STACKIT_NO_AUTH enabled", func() {
208267
It("should create client successfully without authentication", func() {

pkg/client/stackit.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,9 @@ type Server struct {
9090

9191
// NIC represents a STACKIT network interface
9292
type NIC struct {
93-
ID string
94-
NetworkID string
95-
AllowedAddresses []string
93+
ID string `json:"id"`
94+
NetworkID string `json:"networkId"`
95+
AllowedAddresses []string `json:"allowedAddresses,omitempty"`
96+
IPv4 string `json:"ipv4,omitempty"`
97+
IPv6 string `json:"ipv6,omitempty"`
9698
}

pkg/provider/create.go

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/client"
1414
api "github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/provider/apis"
1515
"github.com/stackitcloud/machine-controller-manager-provider-stackit/pkg/provider/apis/validation"
16+
corev1 "k8s.io/api/core/v1"
1617
"k8s.io/apimachinery/pkg/util/wait"
1718
"k8s.io/klog/v2"
1819
"k8s.io/utils/ptr"
@@ -27,10 +28,14 @@ import (
2728
// Returns:
2829
// - ProviderID: Unique identifier in format "stackit://<projectId>/<serverId>"
2930
// - NodeName: Name that the VM will register with in Kubernetes (matches Machine name)
31+
// - Addresses: Internal IP addresses of the server's NICs (NodeInternalIP)
3032
//
31-
// Error codes:
32-
// - InvalidArgument: Invalid ProviderSpec or missing required fields
33-
// - Internal: Failed to create server or communicate with STACKIT API
33+
// Error codes (see machine_error_codes.md for retry semantics):
34+
// - InvalidArgument (no retry): Invalid ProviderSpec fields or missing required values
35+
// - Internal (no retry): Malformed ProviderSpec JSON or failed to initialize STACKIT client
36+
// - Unavailable (retry): Transient API failure (create/get server, get NICs, patch NIC)
37+
// - ResourceExhausted (no retry): No capacity available (e.g. "no valid host was found")
38+
// - DeadlineExceeded (retry): Server did not reach ACTIVE state within the polling timeout
3439
func (p *Provider) CreateMachine(ctx context.Context, req *driver.CreateMachineRequest) (*driver.CreateMachineResponse, error) {
3540
// Log messages to track request
3641
klog.V(2).Infof("Machine creation request has been received for %q", req.Machine.Name)
@@ -89,9 +94,10 @@ func (p *Provider) CreateMachine(ctx context.Context, req *driver.CreateMachineR
8994
return nil, status.Error(codes.DeadlineExceeded, fmt.Sprintf("failed waiting for server to be ACTIVE: %v", err))
9095
}
9196

92-
if err := p.patchNetworkInterface(ctx, projectID, server.ID, providerSpec); err != nil {
93-
klog.Errorf("Failed to patch network interface for server %q: %v", req.Machine.Name, err)
94-
return nil, status.Error(codes.Unavailable, fmt.Sprintf("failed to patch network interface for server: %v", err))
97+
nics, err := p.patchNetworkInterfaces(ctx, projectID, server.ID, providerSpec)
98+
if err != nil {
99+
klog.Errorf("Failed to patch NICs for server %q: %v", req.Machine.Name, err)
100+
return nil, status.Error(codes.Unavailable, fmt.Sprintf("failed to patch NICs for server: %v", err))
95101
}
96102

97103
// Generate ProviderID in format: stackit://<projectId>/<serverId>
@@ -101,6 +107,7 @@ func (p *Provider) CreateMachine(ctx context.Context, req *driver.CreateMachineR
101107
return &driver.CreateMachineResponse{
102108
ProviderID: providerID,
103109
NodeName: req.Machine.Name,
110+
Addresses: nicAddresses(nics),
104111
}, nil
105112
}
106113

@@ -213,6 +220,19 @@ func (p *Provider) createServerRequest(req *driver.CreateMachineRequest, provide
213220
return createReq
214221
}
215222

223+
func nicAddresses(nics []*client.NIC) []corev1.NodeAddress {
224+
var addresses []corev1.NodeAddress
225+
for _, nic := range nics {
226+
if nic.IPv4 != "" {
227+
addresses = append(addresses, corev1.NodeAddress{Type: corev1.NodeInternalIP, Address: nic.IPv4})
228+
}
229+
if nic.IPv6 != "" {
230+
addresses = append(addresses, corev1.NodeAddress{Type: corev1.NodeInternalIP, Address: nic.IPv6})
231+
}
232+
}
233+
return addresses
234+
}
235+
216236
func (p *Provider) getServerByName(ctx context.Context, projectID, region, serverName string) (*client.Server, error) {
217237
// Check if the server got already created
218238
labelSelector := map[string]string{
@@ -235,26 +255,28 @@ func (p *Provider) getServerByName(ctx context.Context, projectID, region, serve
235255
return nil, nil
236256
}
237257

238-
func (p *Provider) patchNetworkInterface(ctx context.Context, projectID, serverID string, providerSpec *api.ProviderSpec) error {
239-
if len(providerSpec.AllowedAddresses) == 0 {
240-
return nil
241-
}
242-
258+
func (p *Provider) patchNetworkInterfaces(ctx context.Context, projectID, serverID string, providerSpec *api.ProviderSpec) ([]*client.NIC, error) {
243259
nics, err := p.client.GetNICsForServer(ctx, projectID, providerSpec.Region, serverID)
244260
if err != nil {
245-
return fmt.Errorf("failed to get NICs for server %q: %w", serverID, err)
261+
return nil, fmt.Errorf("failed to get NICs for server %q: %w", serverID, err)
246262
}
247263

248264
if len(nics) == 0 {
249-
return fmt.Errorf("failed to find NIC for server %q", serverID)
265+
return nil, fmt.Errorf("no NICs found for server %q", serverID)
250266
}
251267

268+
if len(providerSpec.AllowedAddresses) == 0 {
269+
return nics, nil
270+
}
271+
272+
result := make([]*client.NIC, 0, len(nics))
252273
for _, nic := range nics {
253274
// if networking is not set, server is inside the default network
254275
// just patch the interface since the server should only have one
255276
if providerSpec.Networking != nil {
256277
// only process interfaces that are either in the configured network (NetworkID) or are defined in NICIDs
257278
if providerSpec.Networking.NetworkID != nic.NetworkID && !slices.Contains(providerSpec.Networking.NICIDs, nic.ID) {
279+
result = append(result, nic)
258280
continue
259281
}
260282
}
@@ -269,17 +291,20 @@ func (p *Provider) patchNetworkInterface(ctx context.Context, projectID, serverI
269291
}
270292

271293
if !updateNic {
294+
result = append(result, nic)
272295
continue
273296
}
274297

275-
if _, err := p.client.UpdateNIC(ctx, projectID, providerSpec.Region, nic.NetworkID, nic.ID, nic.AllowedAddresses); err != nil {
276-
return fmt.Errorf("failed to update allowed addresses for NIC %s: %w", nic.ID, err)
298+
updatedNic, err := p.client.UpdateNIC(ctx, projectID, providerSpec.Region, nic.NetworkID, nic.ID, nic.AllowedAddresses)
299+
if err != nil {
300+
return nil, fmt.Errorf("failed to update allowed addresses for NIC %s: %w", nic.ID, err)
277301
}
278302

279303
klog.V(2).Infof("Updated allowed addresses for NIC %s to %v", nic.ID, nic.AllowedAddresses)
304+
result = append(result, updatedNic)
280305
}
281306

282-
return nil
307+
return result, nil
283308
}
284309

285310
func (p *Provider) WaitUntilServerRunning(ctx context.Context, projectID, region, serverID string) error {

pkg/provider/create_basic_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,24 @@ var _ = Describe("CreateMachine", func() {
118118
Expect(capturedReq.ImageID).To(Equal("12345678-1234-1234-1234-123456789abc"))
119119
})
120120

121+
It("should return internal IPs from NICs in Addresses", func() {
122+
mockClient.GetNICsFunc = func(_ context.Context, _, _, _ string) ([]*client.NIC, error) {
123+
return []*client.NIC{
124+
{ID: "nic-1", NetworkID: "net-1", IPv4: "10.0.0.5", IPv6: "fd00::1"},
125+
{ID: "nic-2", NetworkID: "net-2", IPv4: "10.0.1.5"},
126+
}, nil
127+
}
128+
129+
resp, err := provider.CreateMachine(ctx, req)
130+
131+
Expect(err).NotTo(HaveOccurred())
132+
Expect(resp.Addresses).To(ConsistOf(
133+
corev1.NodeAddress{Type: corev1.NodeInternalIP, Address: "10.0.0.5"},
134+
corev1.NodeAddress{Type: corev1.NodeInternalIP, Address: "fd00::1"},
135+
corev1.NodeAddress{Type: corev1.NodeInternalIP, Address: "10.0.1.5"},
136+
))
137+
})
138+
121139
It("should poll GetServer until server is ACTIVE", func() {
122140
getServerCallCount := 0
123141

@@ -210,6 +228,22 @@ var _ = Describe("CreateMachine", func() {
210228
})
211229
})
212230

231+
Context("when server has no NICs", func() {
232+
It("should return Unavailable error", func() {
233+
mockClient.GetNICsFunc = func(_ context.Context, _, _, _ string) ([]*client.NIC, error) {
234+
return []*client.NIC{}, nil
235+
}
236+
237+
_, err := provider.CreateMachine(ctx, req)
238+
239+
Expect(err).To(HaveOccurred())
240+
statusErr, ok := status.FromError(err)
241+
Expect(ok).To(BeTrue())
242+
Expect(statusErr.Code()).To(Equal(codes.Unavailable))
243+
Expect(err.Error()).To(ContainSubstring("no NICs found"))
244+
})
245+
})
246+
213247
Context("when STACKIT API fails", func() {
214248
It("should return Internal error on API failure", func() {
215249
mockClient.CreateServerFunc = func(_ context.Context, _, _ string, _ *client.CreateServerRequest) (*client.Server, error) {

0 commit comments

Comments
 (0)