Skip to content

Commit d858af0

Browse files
jwmay2012cpanato
andauthored
Add support for Instance Alias IP Ranges (#1314)
* Add support for Instance Alias IP Ranges * Add unit and validation tests for alias IP ranges - Add unit tests for InstanceNetworkInterfaceAliasIPRangesSpec function - Add CRD validation tests using envtest to verify regex patterns - Tests cover valid formats (CIDR, IP only, netmask only) and invalid cases * Add documentation for alias IP ranges feature * Update copyright year. Co-authored-by: Carlos Tadeu Panato Junior <[email protected]> --------- Co-authored-by: Carlos Tadeu Panato Junior <[email protected]>
1 parent 5ec4d50 commit d858af0

File tree

9 files changed

+521
-0
lines changed

9 files changed

+521
-0
lines changed
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package v1beta1
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"path/filepath"
23+
"testing"
24+
25+
. "github.com/onsi/gomega"
26+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27+
"k8s.io/client-go/kubernetes/scheme"
28+
"sigs.k8s.io/controller-runtime/pkg/client"
29+
"sigs.k8s.io/controller-runtime/pkg/envtest"
30+
)
31+
32+
func TestGCPMachine_AliasIPRanges_Validation(t *testing.T) {
33+
g := NewWithT(t)
34+
ctx := context.Background()
35+
36+
// Setup the test environment with CRDs
37+
testEnv := &envtest.Environment{
38+
CRDDirectoryPaths: []string{
39+
filepath.Join("..", "..", "config", "crd", "bases"),
40+
},
41+
ErrorIfCRDPathMissing: true,
42+
}
43+
44+
cfg, err := testEnv.Start()
45+
g.Expect(err).NotTo(HaveOccurred())
46+
g.Expect(cfg).NotTo(BeNil())
47+
48+
defer func() {
49+
err := testEnv.Stop()
50+
g.Expect(err).NotTo(HaveOccurred())
51+
}()
52+
53+
err = AddToScheme(scheme.Scheme)
54+
g.Expect(err).NotTo(HaveOccurred())
55+
56+
k8sClient, err := client.New(cfg, client.Options{Scheme: scheme.Scheme})
57+
g.Expect(err).NotTo(HaveOccurred())
58+
g.Expect(k8sClient).NotTo(BeNil())
59+
60+
namespace := "default"
61+
62+
tests := []struct {
63+
name string
64+
aliasIPRanges []AliasIPRange
65+
wantErr bool
66+
errorContains string
67+
}{
68+
// Valid cases - these should be accepted
69+
{
70+
name: "valid CIDR notation",
71+
aliasIPRanges: []AliasIPRange{
72+
{
73+
IPCidrRange: "127.0.0.1/24",
74+
SubnetworkRangeName: "subnet-name",
75+
},
76+
},
77+
wantErr: false,
78+
},
79+
{
80+
name: "valid IP address only",
81+
aliasIPRanges: []AliasIPRange{
82+
{
83+
IPCidrRange: "127.0.0.1",
84+
SubnetworkRangeName: "subnet-name",
85+
},
86+
},
87+
wantErr: false,
88+
},
89+
{
90+
name: "valid netmask only",
91+
aliasIPRanges: []AliasIPRange{
92+
{
93+
IPCidrRange: "/24",
94+
SubnetworkRangeName: "subnet-name",
95+
},
96+
},
97+
wantErr: false,
98+
},
99+
{
100+
name: "valid without subnetwork range name",
101+
aliasIPRanges: []AliasIPRange{
102+
{
103+
IPCidrRange: "/24",
104+
},
105+
},
106+
wantErr: false,
107+
},
108+
{
109+
name: "valid multiple ranges",
110+
aliasIPRanges: []AliasIPRange{
111+
{
112+
IPCidrRange: "10.0.0.0/24",
113+
SubnetworkRangeName: "pods",
114+
},
115+
{
116+
IPCidrRange: "10.1.0.0/24",
117+
SubnetworkRangeName: "services",
118+
},
119+
},
120+
wantErr: false,
121+
},
122+
{
123+
name: "valid empty alias IP ranges",
124+
aliasIPRanges: []AliasIPRange{},
125+
wantErr: false,
126+
},
127+
{
128+
name: "valid nil alias IP ranges",
129+
aliasIPRanges: nil,
130+
wantErr: false,
131+
},
132+
// Invalid cases - these should be rejected by CRD validation
133+
{
134+
name: "invalid netmask too large",
135+
aliasIPRanges: []AliasIPRange{
136+
{
137+
IPCidrRange: "/33",
138+
SubnetworkRangeName: "subnet-name",
139+
},
140+
},
141+
wantErr: true,
142+
errorContains: "should match",
143+
},
144+
{
145+
name: "invalid empty ipCidrRange",
146+
aliasIPRanges: []AliasIPRange{
147+
{
148+
IPCidrRange: "",
149+
SubnetworkRangeName: "subnet-name",
150+
},
151+
},
152+
wantErr: true,
153+
errorContains: "should match",
154+
},
155+
{
156+
name: "invalid IP address out of range",
157+
aliasIPRanges: []AliasIPRange{
158+
{
159+
IPCidrRange: "1270.0.0.1/24",
160+
SubnetworkRangeName: "subnet-name",
161+
},
162+
},
163+
wantErr: true,
164+
errorContains: "should match",
165+
},
166+
{
167+
name: "invalid IP address with letters",
168+
aliasIPRanges: []AliasIPRange{
169+
{
170+
IPCidrRange: "127.0.0.1a",
171+
SubnetworkRangeName: "subnet-name",
172+
},
173+
},
174+
wantErr: true,
175+
errorContains: "should match",
176+
},
177+
{
178+
name: "invalid CIDR with letters",
179+
aliasIPRanges: []AliasIPRange{
180+
{
181+
IPCidrRange: "127.0.0.1a/24",
182+
SubnetworkRangeName: "subnet-name",
183+
},
184+
},
185+
wantErr: true,
186+
errorContains: "should match",
187+
},
188+
{
189+
name: "invalid format with extra slash",
190+
aliasIPRanges: []AliasIPRange{
191+
{
192+
IPCidrRange: "10.0.0.0//24",
193+
SubnetworkRangeName: "subnet-name",
194+
},
195+
},
196+
wantErr: true,
197+
errorContains: "should match",
198+
},
199+
{
200+
name: "invalid format with space",
201+
aliasIPRanges: []AliasIPRange{
202+
{
203+
IPCidrRange: "10.0.0.0 /24",
204+
SubnetworkRangeName: "subnet-name",
205+
},
206+
},
207+
wantErr: true,
208+
errorContains: "should match",
209+
},
210+
}
211+
212+
for i, tt := range tests {
213+
t.Run(tt.name, func(t *testing.T) {
214+
g := NewWithT(t)
215+
216+
// Create a GCPMachine with the test aliasIPRanges
217+
machine := &GCPMachine{
218+
ObjectMeta: metav1.ObjectMeta{
219+
Name: fmt.Sprintf("test-machine-%d", i),
220+
Namespace: namespace,
221+
},
222+
Spec: GCPMachineSpec{
223+
InstanceType: "n1-standard-2",
224+
AliasIPRanges: tt.aliasIPRanges,
225+
},
226+
}
227+
228+
// Attempt to create the machine
229+
err := k8sClient.Create(ctx, machine)
230+
231+
if tt.wantErr {
232+
g.Expect(err).To(HaveOccurred())
233+
if tt.errorContains != "" {
234+
g.Expect(err.Error()).To(ContainSubstring(tt.errorContains))
235+
}
236+
} else {
237+
g.Expect(err).NotTo(HaveOccurred())
238+
// Clean up successfully created resources
239+
_ = k8sClient.Delete(ctx, machine)
240+
}
241+
})
242+
}
243+
}

api/v1beta1/gcpmachine_types.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,23 @@ const (
236236
ProvisioningModelSpot ProvisioningModel = "Spot"
237237
)
238238

239+
// AliasIPRange is an alias IP range attached to an instance's network interface.
240+
type AliasIPRange struct {
241+
// IPCidrRange is the IP alias ranges to allocate for this interface. This IP
242+
// CIDR range must belong to the specified subnetwork and cannot contain IP
243+
// addresses reserved by system or used by other network interfaces. This range
244+
// may be a single IP address (such as 10.2.3.4), a netmask (such as /24) or a
245+
// CIDR-formatted string (such as 10.1.2.0/24).
246+
// +kubebuilder:validation:Pattern=`^((([0-9]|[0-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[0-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])/([0-9]|[12][0-9]|3[0-2])|(([0-9]|[0-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[0-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|(/([0-9]|[12][0-9]|3[0-2])))$`
247+
// +required
248+
IPCidrRange string `json:"ipCidrRange"`
249+
// SubnetworkRangeName is the name of a subnetwork secondary IP range from which
250+
// to allocate an IP alias range. If not specified, the primary range of the
251+
// subnetwork is used.
252+
// +optional
253+
SubnetworkRangeName string `json:"subnetworkRangeName,omitempty"`
254+
}
255+
239256
// GCPMachineSpec defines the desired state of GCPMachine.
240257
type GCPMachineSpec struct {
241258
// InstanceType is the type of instance to create. Example: n1.standard-2
@@ -246,6 +263,10 @@ type GCPMachineSpec struct {
246263
// +optional
247264
Subnet *string `json:"subnet,omitempty"`
248265

266+
// AliasIPRanges let you assign ranges of internal IP addresses as aliases to a VM's network interfaces.
267+
// +optional
268+
AliasIPRanges []AliasIPRange `json:"aliasIPRanges,omitempty"`
269+
249270
// ProviderID is the unique identifier as specified by the cloud provider.
250271
// +optional
251272
ProviderID *string `json:"providerID,omitempty"`

api/v1beta1/zz_generated.deepcopy.go

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cloud/scope/machine.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,9 +345,27 @@ func (m *MachineScope) InstanceNetworkInterfaceSpec() *compute.NetworkInterface
345345
networkInterface.Subnetwork = path.Join("projects", m.ClusterGetter.NetworkProject(), "regions", m.ClusterGetter.Region(), "subnetworks", *m.GCPMachine.Spec.Subnet)
346346
}
347347

348+
networkInterface.AliasIpRanges = m.InstanceNetworkInterfaceAliasIPRangesSpec()
349+
348350
return networkInterface
349351
}
350352

353+
// InstanceNetworkInterfaceAliasIPRangesSpec returns a slice of Alias IP Range specs.
354+
func (m *MachineScope) InstanceNetworkInterfaceAliasIPRangesSpec() []*compute.AliasIpRange {
355+
if len(m.GCPMachine.Spec.AliasIPRanges) == 0 {
356+
return nil
357+
}
358+
aliasIPRanges := make([]*compute.AliasIpRange, 0, len(m.GCPMachine.Spec.AliasIPRanges))
359+
for _, alias := range m.GCPMachine.Spec.AliasIPRanges {
360+
aliasIPRange := &compute.AliasIpRange{
361+
IpCidrRange: alias.IPCidrRange,
362+
SubnetworkRangeName: alias.SubnetworkRangeName,
363+
}
364+
aliasIPRanges = append(aliasIPRanges, aliasIPRange)
365+
}
366+
return aliasIPRanges
367+
}
368+
351369
// instanceServiceAccountsSpec returns service-account spec.
352370
func instanceServiceAccountsSpec(serviceAccount *infrav1.ServiceAccount) *compute.ServiceAccount {
353371
out := &compute.ServiceAccount{

0 commit comments

Comments
 (0)