Skip to content

Commit e85c011

Browse files
committed
Adding dual-stack support for Cluster API GCP Provider
api/v1beta/types.go: Adding support for machines and simple network changes for IPV6 or dual stack work. - InternalIpv6PrefixLength - IPv6Address - StackType cloud/scope/machine.go: Adding support for instances and networks to allow dual stack components. - InstanceNetworkInterfaceSpec - InternalIpv6PrefixLength - Ipv6AccessConfigs - ExternalIPv6 - ExternalIpv6PrefixLength - Type (when present always set to DIRECT_IPV6) - Name (always set to External IPv6) - IPv6AccessType - Ipv6Address - StackType - InstanceSpec - PrivateIpv6GoogleAccess - InstanceNetworkInterfaceAliasIPRangesSpec ** This did not change. The AliasIPs appear to only support IPv4 CIDR or single address format. cloud/interfaces.go: Expose a couple of new functions to get StackType and IPvAddress information from the cluster specs. These are only getter functions for the Machine.go to access.
1 parent 958d55b commit e85c011

10 files changed

+297
-43
lines changed

api/v1beta1/types.go

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,18 @@ const (
137137
RulesManagementUnmanaged RulesManagementPolicy = "Unmanaged"
138138
)
139139

140+
// StackType is a string enum type indicating the types of network addresses that are valid.
141+
// +kubebuilder:validation:Enum=IPV4_ONLY;IPV4_IPV6
142+
type StackType string
143+
144+
const (
145+
// IPv4OnlyStackType indicates a stack type where only IPv4 addresses are valid.
146+
IPv4OnlyStackType StackType = "IPV4_ONLY"
147+
148+
// DualStackType indicates a stack type where both IPv4 and IPv6 addresses are valid.
149+
DualStackType StackType = "IPV4_IPV6"
150+
)
151+
140152
// NetworkSpec encapsulates all things related to a GCP network.
141153
type NetworkSpec struct {
142154
// Name is the name of the network to be used.
@@ -191,6 +203,28 @@ type NetworkSpec struct {
191203
// +kubebuilder:default:=64
192204
// +optional
193205
MinPortsPerVM int64 `json:"minPortsPerVm,omitempty"`
206+
207+
// InternalIpv6PrefixLength: The prefix length of the primary internal IPv6 range.
208+
// +kubebuilder:validation:Minimum=0
209+
// +kubebuilder:validation:Maximum=128
210+
// +optional
211+
InternalIpv6PrefixLength int `json:"internalIpv6PrefixLength,omitempty"`
212+
213+
// Ipv6Address: An IPv6 internal network address for this network interface.
214+
// To use a static internal IP address, it must be unused and in the same
215+
// region as the instance's zone. If not specified, Google Cloud will
216+
// automatically assign an internal IPv6 address from the instance's subnetwork.
217+
// +optional
218+
Ipv6Address string `json:"ipv6Address,omitempty"`
219+
220+
// StackType: The stack type for the subnet. If set to IPV4_ONLY, new VMs in
221+
// the subnet are assigned IPv4 addresses only. If set to IPV4_IPV6, new VMs in
222+
// the subnet can be assigned both IPv4 and IPv6 addresses. If not specified,
223+
// IPV4_ONLY is used. This field can be both set at resource creation time and
224+
// updated using patch.
225+
// +kubebuilder:default=IPV4_ONLY
226+
// +optional
227+
StackType StackType `json:"stackType,omitempty"`
194228
}
195229

196230
// LoadBalancerType defines the Load Balancer that should be created.
@@ -290,16 +324,9 @@ type SubnetSpec struct {
290324
// the subnet can be assigned both IPv4 and IPv6 addresses. If not specified,
291325
// IPV4_ONLY is used. This field can be both set at resource creation time and
292326
// updated using patch.
293-
//
294-
// Possible values:
295-
// "IPV4_IPV6" - New VMs in this subnet can have both IPv4 and IPv6
296-
// addresses.
297-
// "IPV4_ONLY" - New VMs in this subnet will only be assigned IPv4 addresses.
298-
// "IPV6_ONLY" - New VMs in this subnet will only be assigned IPv6 addresses.
299-
// +kubebuilder:validation:Enum=IPV4_ONLY;IPV4_IPV6;IPV6_ONLY
300327
// +kubebuilder:default=IPV4_ONLY
301328
// +optional
302-
StackType string `json:"stackType,omitempty"`
329+
StackType StackType `json:"stackType,omitempty"`
303330
}
304331

305332
// String returns a string representation of the subnet.

cloud/interfaces.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ type ClusterGetter interface {
5858
NetworkName() string
5959
NetworkProject() string
6060
IsSharedVpc() bool
61+
StackType() infrav1.StackType
62+
Ipv6Address() string
6163
SkipFirewallRuleCreation() bool
6264
Network() *infrav1.Network
6365
AdditionalLabels() infrav1.Labels

cloud/scope/cluster.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,20 @@ func (s *ClusterScope) IsSharedVpc() bool {
120120
return s.NetworkProject() != s.Project()
121121
}
122122

123+
// StackType returns the network stack type for the cluster.
124+
func (s *ClusterScope) StackType() infrav1.StackType {
125+
return s.GCPCluster.Spec.Network.StackType
126+
}
127+
128+
// Ipv6Address returns the IPv6 address when one is provided in the spec and the cluster network is not IPv4 Only.
129+
func (s *ClusterScope) Ipv6Address() string {
130+
address := ""
131+
if s.StackType() != infrav1.IPv4OnlyStackType {
132+
address = s.GCPCluster.Spec.Network.Ipv6Address
133+
}
134+
return address
135+
}
136+
123137
// Region returns the cluster region.
124138
func (s *ClusterScope) Region() string {
125139
return s.GCPCluster.Spec.Region
@@ -283,7 +297,7 @@ func (s *ClusterScope) SubnetSpecs() []*compute.Subnetwork {
283297
Network: s.NetworkLink(),
284298
Purpose: ptr.Deref(subnetwork.Purpose, "PRIVATE_RFC_1918"),
285299
Role: "ACTIVE",
286-
StackType: subnetwork.StackType,
300+
StackType: string(subnetwork.StackType),
287301
})
288302
}
289303

cloud/scope/machine.go

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -326,12 +326,31 @@ func (m *MachineScope) InstanceNetworkInterfaceSpec() *compute.NetworkInterface
326326
}
327327

328328
if m.GCPMachine.Spec.PublicIP != nil && *m.GCPMachine.Spec.PublicIP {
329-
networkInterface.AccessConfigs = []*compute.AccessConfig{
330-
{
331-
Type: "ONE_TO_ONE_NAT",
332-
Name: "External NAT",
333-
},
329+
switch m.ClusterGetter.StackType() {
330+
case infrav1.IPv4OnlyStackType:
331+
networkInterface.AccessConfigs = []*compute.AccessConfig{
332+
{
333+
Type: "ONE_TO_ONE_NAT",
334+
Name: "External NAT",
335+
},
336+
}
337+
case infrav1.DualStackType:
338+
networkInterface.Ipv6AccessConfigs = []*compute.AccessConfig{
339+
{
340+
Type: "DIRECT_IPV6",
341+
Name: "External IPv6",
342+
},
343+
}
344+
}
345+
}
346+
347+
if m.ClusterGetter.StackType() == infrav1.DualStackType {
348+
accessType := "INTERNAL"
349+
if m.GCPMachine.Spec.PublicIP != nil && *m.GCPMachine.Spec.PublicIP {
350+
accessType = "EXTERNAL"
334351
}
352+
networkInterface.Ipv6AccessType = accessType
353+
networkInterface.Ipv6Address = m.ClusterGetter.Ipv6Address()
335354
}
336355

337356
if m.GCPMachine.Spec.Subnet != nil {
@@ -504,6 +523,10 @@ func (m *MachineScope) InstanceSpec(log logr.Logger) *compute.Instance {
504523
instance.Scheduling.OnHostMaintenance = "TERMINATE"
505524
}
506525

526+
if m.ClusterGetter.StackType() == infrav1.DualStackType {
527+
instance.PrivateIpv6GoogleAccess = "INHERIT_FROM_SUBNETWORK"
528+
}
529+
507530
return instance
508531
}
509532

cloud/scope/managedcluster.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,20 @@ func (s *ManagedClusterScope) IsSharedVpc() bool {
143143
return s.NetworkProject() != s.Project()
144144
}
145145

146+
// StackType returns the network stack type for the cluster.
147+
func (s *ManagedClusterScope) StackType() infrav1.StackType {
148+
return s.GCPManagedCluster.Spec.Network.StackType
149+
}
150+
151+
// Ipv6Address returns the IPv6 address when one is provided in the spec and the cluster network is not IPv4 Only.
152+
func (s *ManagedClusterScope) Ipv6Address() string {
153+
address := ""
154+
if s.StackType() != infrav1.IPv4OnlyStackType {
155+
address = s.GCPManagedCluster.Spec.Network.Ipv6Address
156+
}
157+
return address
158+
}
159+
146160
// NetworkLink returns the partial URL for the network.
147161
func (s *ManagedClusterScope) NetworkLink() string {
148162
return fmt.Sprintf("projects/%s/global/networks/%s", s.NetworkProject(), s.NetworkName())
@@ -269,7 +283,7 @@ func (s *ManagedClusterScope) SubnetSpecs() []*compute.Subnetwork {
269283
Network: s.NetworkLink(),
270284
Purpose: ptr.Deref(subnetwork.Purpose, "PRIVATE_RFC_1918"),
271285
Role: "ACTIVE",
272-
StackType: subnetwork.StackType,
286+
StackType: string(subnetwork.StackType),
273287
})
274288
}
275289

cloud/services/compute/instances/reconcile_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,20 @@ var fakeGCPCluster = &infrav1.GCPCluster{
119119
},
120120
}
121121

122+
var fakeGCPClusterIpv6 = &infrav1.GCPCluster{
123+
ObjectMeta: metav1.ObjectMeta{
124+
Name: "my-cluster",
125+
Namespace: "default",
126+
},
127+
Spec: infrav1.GCPClusterSpec{
128+
Project: "my-proj",
129+
Region: "us-central1",
130+
Network: infrav1.NetworkSpec{
131+
StackType: infrav1.DualStackType,
132+
},
133+
},
134+
}
135+
122136
func getFakeGCPMachine() *infrav1.GCPMachine {
123137
return &infrav1.GCPMachine{
124138
ObjectMeta: metav1.ObjectMeta{
@@ -154,6 +168,18 @@ func TestService_createOrGetInstance(t *testing.T) {
154168
t.Fatal(err)
155169
}
156170

171+
clusterScopeIpv6, err := scope.NewClusterScope(context.TODO(), scope.ClusterScopeParams{
172+
Client: fakec,
173+
Cluster: fakeCluster,
174+
GCPCluster: fakeGCPClusterIpv6,
175+
GCPServices: scope.GCPServices{
176+
Compute: &compute.Service{},
177+
},
178+
})
179+
if err != nil {
180+
t.Fatal(err)
181+
}
182+
157183
machineScope, err := scope.NewMachineScope(scope.MachineScopeParams{
158184
Client: fakec,
159185
Machine: fakeMachine,
@@ -164,6 +190,16 @@ func TestService_createOrGetInstance(t *testing.T) {
164190
t.Fatal(err)
165191
}
166192

193+
machineScopeIpv6, err := scope.NewMachineScope(scope.MachineScopeParams{
194+
Client: fakec,
195+
Machine: fakeMachine,
196+
GCPMachine: fakeGCPMachine,
197+
ClusterGetter: clusterScopeIpv6,
198+
})
199+
if err != nil {
200+
t.Fatal(err)
201+
}
202+
167203
clusterScopeWithoutFailureDomain, err := scope.NewClusterScope(context.TODO(), scope.ClusterScopeParams{
168204
Client: fakec,
169205
Cluster: fakeCluster,
@@ -1086,6 +1122,72 @@ func TestService_createOrGetInstance(t *testing.T) {
10861122
Zone: "us-central1-c",
10871123
},
10881124
},
1125+
{
1126+
name: "ipv6 instance does not exist (should create instance)",
1127+
scope: func() Scope { return machineScopeIpv6 },
1128+
mockInstance: &cloud.MockInstances{
1129+
ProjectRouter: &cloud.SingleProjectRouter{ID: "proj-id"},
1130+
Objects: map[meta.Key]*cloud.MockInstancesObj{},
1131+
},
1132+
want: &compute.Instance{
1133+
Name: "my-machine",
1134+
CanIpForward: true,
1135+
Disks: []*compute.AttachedDisk{
1136+
{
1137+
AutoDelete: true,
1138+
Boot: true,
1139+
InitializeParams: &compute.AttachedDiskInitializeParams{
1140+
DiskType: "zones/us-central1-c/diskTypes/pd-standard",
1141+
SourceImage: "projects/my-proj/global/images/family/capi-ubuntu-1804-k8s-v1-19",
1142+
ResourceManagerTags: map[string]string{},
1143+
Labels: map[string]string{
1144+
"foo": "bar",
1145+
},
1146+
},
1147+
},
1148+
},
1149+
Labels: map[string]string{
1150+
"capg-role": "node",
1151+
"capg-cluster-my-cluster": "owned",
1152+
"foo": "bar",
1153+
},
1154+
MachineType: "zones/us-central1-c/machineTypes",
1155+
Metadata: &compute.Metadata{
1156+
Items: []*compute.MetadataItems{
1157+
{
1158+
Key: "user-data",
1159+
Value: ptr.To[string]("Zm9vCg=="),
1160+
},
1161+
},
1162+
},
1163+
NetworkInterfaces: []*compute.NetworkInterface{
1164+
{
1165+
Network: "projects/my-proj/global/networks/default",
1166+
Ipv6AccessType: "INTERNAL", // default, this was not overridden with a public ip address set.
1167+
Ipv6Address: "",
1168+
},
1169+
},
1170+
Params: &compute.InstanceParams{
1171+
ResourceManagerTags: map[string]string{},
1172+
},
1173+
PrivateIpv6GoogleAccess: "INHERIT_FROM_SUBNETWORK",
1174+
SelfLink: "https://www.googleapis.com/compute/v1/projects/proj-id/zones/us-central1-c/instances/my-machine",
1175+
Scheduling: &compute.Scheduling{},
1176+
ServiceAccounts: []*compute.ServiceAccount{
1177+
{
1178+
Email: "default",
1179+
Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"},
1180+
},
1181+
},
1182+
Tags: &compute.Tags{
1183+
Items: []string{
1184+
"my-cluster-node",
1185+
"my-cluster",
1186+
},
1187+
},
1188+
Zone: "us-central1-c",
1189+
},
1190+
},
10891191
}
10901192
for _, tt := range tests {
10911193
t.Run(tt.name, func(t *testing.T) {

config/crd/bases/infrastructure.cluster.x-k8s.io_gcpclusters.yaml

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,19 @@ spec:
203203
description: HostProject is the name of the project hosting the
204204
shared VPC network resources.
205205
type: string
206+
internalIpv6PrefixLength:
207+
description: 'InternalIpv6PrefixLength: The prefix length of the
208+
primary internal IPv6 range.'
209+
maximum: 128
210+
minimum: 0
211+
type: integer
212+
ipv6Address:
213+
description: |-
214+
Ipv6Address: An IPv6 internal network address for this network interface.
215+
To use a static internal IP address, it must be unused and in the same
216+
region as the instance's zone. If not specified, Google Cloud will
217+
automatically assign an internal IPv6 address from the instance's subnetwork.
218+
type: string
206219
loadBalancerBackendPort:
207220
description: Allow for configuration of load balancer backend
208221
(useful for changing apiserver port)
@@ -234,6 +247,18 @@ spec:
234247
name:
235248
description: Name is the name of the network to be used.
236249
type: string
250+
stackType:
251+
default: IPV4_ONLY
252+
description: |-
253+
StackType: The stack type for the subnet. If set to IPV4_ONLY, new VMs in
254+
the subnet are assigned IPv4 addresses only. If set to IPV4_IPV6, new VMs in
255+
the subnet can be assigned both IPv4 and IPv6 addresses. If not specified,
256+
IPV4_ONLY is used. This field can be both set at resource creation time and
257+
updated using patch.
258+
enum:
259+
- IPV4_ONLY
260+
- IPV4_IPV6
261+
type: string
237262
subnets:
238263
description: Subnets configuration.
239264
items:
@@ -309,16 +334,9 @@ spec:
309334
the subnet can be assigned both IPv4 and IPv6 addresses. If not specified,
310335
IPV4_ONLY is used. This field can be both set at resource creation time and
311336
updated using patch.
312-
313-
Possible values:
314-
"IPV4_IPV6" - New VMs in this subnet can have both IPv4 and IPv6
315-
addresses.
316-
"IPV4_ONLY" - New VMs in this subnet will only be assigned IPv4 addresses.
317-
"IPV6_ONLY" - New VMs in this subnet will only be assigned IPv6 addresses.
318337
enum:
319338
- IPV4_ONLY
320339
- IPV4_IPV6
321-
- IPV6_ONLY
322340
type: string
323341
type: object
324342
type: array

0 commit comments

Comments
 (0)