Skip to content

Commit 105c296

Browse files
authored
Merge pull request #740 from cprivitere/add-emlb-support
✨ Add Equinix Metal Load Balancer support
2 parents be9da2d + 4ca3b76 commit 105c296

File tree

57 files changed

+11159
-61
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+11159
-61
lines changed

.golangci.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,6 @@ linters-settings:
164164
alias: infraexpv1
165165
nolintlint:
166166
allow-unused: false
167-
allow-leading-space: false
168167
require-specific: true
169168
revive:
170169
rules:
@@ -314,3 +313,7 @@ issues:
314313
- gocritic
315314
text: "deferInLoop: Possible resource leak, 'defer' is called in the 'for' loop"
316315
path: _test\.go
316+
- linters:
317+
- bodyclose
318+
path: .*(internal)/emlb/emlb.go
319+
text: "response body must be closed"

api/v1beta1/packetcluster_types.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const (
2626
NetworkInfrastructureReadyCondition clusterv1.ConditionType = "NetworkInfrastructureReady"
2727
)
2828

29-
// VIPManagerType describes if the VIP will be managed by CPEM or kube-vip.
29+
// VIPManagerType describes if the VIP will be managed by CPEM or kube-vip or Equinix Metal Load Balancer.
3030
type VIPManagerType string
3131

3232
// PacketClusterSpec defines the desired state of PacketCluster.
@@ -46,9 +46,9 @@ type PacketClusterSpec struct {
4646
// +optional
4747
ControlPlaneEndpoint clusterv1.APIEndpoint `json:"controlPlaneEndpoint"`
4848

49-
// VIPManager represents whether this cluster uses CPEM or kube-vip to
49+
// VIPManager represents whether this cluster uses CPEM or kube-vip or Equinix Metal Load Balancer to
5050
// manage its vip for the api server IP
51-
// +kubebuilder:validation:Enum=CPEM;KUBE_VIP
51+
// +kubebuilder:validation:Enum=CPEM;KUBE_VIP;EMLB
5252
// +kubebuilder:default:=CPEM
5353
VIPManager VIPManagerType `json:"vipManager"`
5454
}

api/v1beta1/packetcluster_webhook.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ func (c *PacketCluster) ValidateUpdate(oldRaw runtime.Object) (admission.Warning
9494
}
9595

9696
// Must have only one of Metro or Facility
97-
if len(c.Spec.Facility) > 0 && len(c.Spec.Metro) > 0 {
97+
if c.Spec.Facility != "" && c.Spec.Metro != "" {
9898
allErrs = append(allErrs,
9999
field.Invalid(field.NewPath("spec", "Facility"),
100100
c.Spec.Facility, "Metro and Facility are mutually exclusive, Metro is recommended"),

api/v1beta1/packetmachine_webhook.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func (m *PacketMachine) ValidateUpdate(old runtime.Object) (admission.Warnings,
5353
var allErrs field.ErrorList
5454

5555
// Must have only one of Metro or Facility specified
56-
if len(m.Spec.Facility) > 0 && len(m.Spec.Metro) > 0 {
56+
if m.Spec.Facility != "" && m.Spec.Metro != "" {
5757
allErrs = append(allErrs,
5858
field.Invalid(field.NewPath("spec", "Facility"),
5959
m.Spec.Facility, "Metro and Facility field are mutually exclusive"),

clusterctl-settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "infrastructure-packet",
33
"config": {
44
"componentsFile": "infrastructure-components.yaml",
5-
"nextVersion": "v0.6.99"
5+
"nextVersion": "v0.8.99"
66
}
77
}
88

cmd/ci-clean/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func main() {
4343
rootCmd := &cobra.Command{ //nolint:exhaustivestruct
4444
Use: "ci-clean",
4545
Short: "Clean up any stray resources in CI",
46-
RunE: func(cmd *cobra.Command, args []string) error {
46+
RunE: func(_ *cobra.Command, _ []string) error {
4747
metalAuthToken := os.Getenv(authTokenEnvVar)
4848
if metalAuthToken == "" {
4949
return fmt.Errorf("%s: %w", authTokenEnvVar, errMissingRequiredEnvVar)

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,12 @@ spec:
8080
vipManager:
8181
default: CPEM
8282
description: |-
83-
VIPManager represents whether this cluster uses CPEM or kube-vip to
83+
VIPManager represents whether this cluster uses CPEM or kube-vip or Equinix Metal Load Balancer to
8484
manage its vip for the api server IP
8585
enum:
8686
- CPEM
8787
- KUBE_VIP
88+
- EMLB
8889
type: string
8990
required:
9091
- projectID

config/manager/manager.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,10 @@ spec:
4242
port: healthz
4343
resources:
4444
limits:
45-
memory: 200Mi
45+
memory: 300Mi
4646
requests:
4747
cpu: 100m
48-
memory: 200Mi
48+
memory: 300Mi
4949
securityContext:
5050
allowPrivilegeEscalation: false
5151
capabilities:

controllers/packetcluster_controller.go

Lines changed: 48 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"sigs.k8s.io/controller-runtime/pkg/handler"
3535

3636
infrav1 "sigs.k8s.io/cluster-api-provider-packet/api/v1beta1"
37+
"sigs.k8s.io/cluster-api-provider-packet/internal/emlb"
3738
packet "sigs.k8s.io/cluster-api-provider-packet/pkg/cloud/packet"
3839
"sigs.k8s.io/cluster-api-provider-packet/pkg/cloud/packet/scope"
3940
)
@@ -114,49 +115,63 @@ func (r *PacketClusterReconciler) reconcileNormal(ctx context.Context, clusterSc
114115

115116
packetCluster := clusterScope.PacketCluster
116117

117-
ipReserv, err := r.PacketClient.GetIPByClusterIdentifier(ctx, clusterScope.Namespace(), clusterScope.Name(), packetCluster.Spec.ProjectID)
118118
switch {
119-
case errors.Is(err, packet.ErrControlPlanEndpointNotFound):
120-
// Parse metro and facility from the cluster spec
121-
var metro, facility string
122-
123-
facility = packetCluster.Spec.Facility
124-
metro = packetCluster.Spec.Metro
125-
126-
// If both specified, metro takes precedence over facility
127-
if metro != "" {
128-
facility = ""
119+
case packetCluster.Spec.VIPManager == emlb.EMLBVIPID:
120+
if !packetCluster.Spec.ControlPlaneEndpoint.IsValid() {
121+
// Create new EMLB object
122+
lb := emlb.NewEMLB(r.PacketClient.GetConfig().DefaultHeader["X-Auth-Token"], packetCluster.Spec.ProjectID, packetCluster.Spec.Metro)
123+
124+
if err := lb.ReconcileLoadBalancer(ctx, clusterScope); err != nil {
125+
log.Error(err, "Error Reconciling EMLB")
126+
return err
127+
}
129128
}
130-
131-
// There is not an ElasticIP with the right tags, at this point we can create one
132-
ip, err := r.PacketClient.CreateIP(ctx, clusterScope.Namespace(), clusterScope.Name(), packetCluster.Spec.ProjectID, facility, metro)
133-
if err != nil {
134-
log.Error(err, "error reserving an ip")
129+
case packetCluster.Spec.VIPManager == "KUBE_VIP":
130+
log.Info("KUBE_VIP VIPManager Detected")
131+
if err := r.PacketClient.EnableProjectBGP(ctx, packetCluster.Spec.ProjectID); err != nil {
132+
log.Error(err, "error enabling bgp for project")
135133
return err
136134
}
137-
clusterScope.PacketCluster.Spec.ControlPlaneEndpoint = clusterv1.APIEndpoint{
138-
Host: ip.To4().String(),
139-
Port: 6443,
140-
}
141-
case err != nil:
142-
log.Error(err, "error getting cluster IP")
143-
return err
144-
default:
145-
// If there is an ElasticIP with the right tag just use it again
146-
clusterScope.PacketCluster.Spec.ControlPlaneEndpoint = clusterv1.APIEndpoint{
147-
Host: ipReserv.GetAddress(),
148-
Port: 6443,
149-
}
150135
}
151136

152-
if clusterScope.PacketCluster.Spec.VIPManager == "KUBE_VIP" {
153-
if err := r.PacketClient.EnableProjectBGP(ctx, packetCluster.Spec.ProjectID); err != nil {
154-
log.Error(err, "error enabling bgp for project")
137+
if packetCluster.Spec.VIPManager != emlb.EMLBVIPID {
138+
ipReserv, err := r.PacketClient.GetIPByClusterIdentifier(ctx, clusterScope.Namespace(), clusterScope.Name(), packetCluster.Spec.ProjectID)
139+
switch {
140+
case errors.Is(err, packet.ErrControlPlanEndpointNotFound):
141+
// Parse metro and facility from the cluster spec
142+
var metro, facility string
143+
144+
facility = packetCluster.Spec.Facility
145+
metro = packetCluster.Spec.Metro
146+
147+
// If both specified, metro takes precedence over facility
148+
if metro != "" {
149+
facility = ""
150+
}
151+
152+
// There is not an ElasticIP with the right tags, at this point we can create one
153+
ip, err := r.PacketClient.CreateIP(ctx, clusterScope.Namespace(), clusterScope.Name(), packetCluster.Spec.ProjectID, facility, metro)
154+
if err != nil {
155+
log.Error(err, "error reserving an ip")
156+
return err
157+
}
158+
packetCluster.Spec.ControlPlaneEndpoint = clusterv1.APIEndpoint{
159+
Host: ip.To4().String(),
160+
Port: 6443,
161+
}
162+
case err != nil:
163+
log.Error(err, "error getting cluster IP")
155164
return err
165+
default:
166+
// If there is an ElasticIP with the right tag just use it again
167+
packetCluster.Spec.ControlPlaneEndpoint = clusterv1.APIEndpoint{
168+
Host: ipReserv.GetAddress(),
169+
Port: 6443,
170+
}
156171
}
157172
}
158173

159-
clusterScope.PacketCluster.Status.Ready = true
174+
packetCluster.Status.Ready = true
160175
conditions.MarkTrue(packetCluster, infrav1.NetworkInfrastructureReadyCondition)
161176

162177
return nil

controllers/packetmachine_controller.go

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import (
4343
"sigs.k8s.io/controller-runtime/pkg/reconcile"
4444

4545
infrav1 "sigs.k8s.io/cluster-api-provider-packet/api/v1beta1"
46+
"sigs.k8s.io/cluster-api-provider-packet/internal/emlb"
4647
packet "sigs.k8s.io/cluster-api-provider-packet/pkg/cloud/packet"
4748
"sigs.k8s.io/cluster-api-provider-packet/pkg/cloud/packet/scope"
4849
clog "sigs.k8s.io/cluster-api/util/log"
@@ -348,23 +349,33 @@ func (r *PacketMachineReconciler) reconcile(ctx context.Context, machineScope *s
348349
// when a node is a control plane node we need the elastic IP
349350
// to template out the kube-vip deployment
350351
if machineScope.IsControlPlane() {
351-
controlPlaneEndpoint, _ = r.PacketClient.GetIPByClusterIdentifier(
352-
ctx,
353-
machineScope.Cluster.Namespace,
354-
machineScope.Cluster.Name,
355-
machineScope.PacketCluster.Spec.ProjectID)
356-
if machineScope.PacketCluster.Spec.VIPManager == "CPEM" {
352+
var controlPlaneEndpointAddress string
353+
var cpemLBConfig string
354+
var emlbID string
355+
switch {
356+
case machineScope.PacketCluster.Spec.VIPManager == "CPEM":
357+
controlPlaneEndpoint, _ = r.PacketClient.GetIPByClusterIdentifier(
358+
ctx,
359+
machineScope.Cluster.Namespace,
360+
machineScope.Cluster.Name,
361+
machineScope.PacketCluster.Spec.ProjectID)
357362
if len(controlPlaneEndpoint.Assignments) == 0 {
358363
a := corev1.NodeAddress{
359364
Type: corev1.NodeExternalIP,
360365
Address: controlPlaneEndpoint.GetAddress(),
361366
}
362367
addrs = append(addrs, a)
363368
}
369+
controlPlaneEndpointAddress = controlPlaneEndpoint.GetAddress()
370+
case machineScope.PacketCluster.Spec.VIPManager == emlb.EMLBVIPID:
371+
controlPlaneEndpointAddress = machineScope.Cluster.Spec.ControlPlaneEndpoint.Host
372+
cpemLBConfig = "emlb:///" + machineScope.PacketCluster.Spec.Metro
373+
emlbID = machineScope.PacketCluster.Annotations["equinix.com/loadbalancerID"]
364374
}
365-
createDeviceReq.ControlPlaneEndpoint = controlPlaneEndpoint.GetAddress()
375+
createDeviceReq.ControlPlaneEndpoint = controlPlaneEndpointAddress
376+
createDeviceReq.CPEMLBConfig = cpemLBConfig
377+
createDeviceReq.EMLBID = emlbID
366378
}
367-
368379
dev, err = r.PacketClient.NewDevice(ctx, createDeviceReq)
369380

370381
switch {
@@ -413,7 +424,8 @@ func (r *PacketMachineReconciler) reconcile(ctx context.Context, machineScope *s
413424
case infrav1.PacketResourceStatusRunning:
414425
log.Info("Machine instance is active", "instance-id", machineScope.ProviderID())
415426

416-
if machineScope.PacketCluster.Spec.VIPManager == "CPEM" {
427+
switch {
428+
case machineScope.PacketCluster.Spec.VIPManager == "CPEM":
417429
controlPlaneEndpoint, _ = r.PacketClient.GetIPByClusterIdentifier(
418430
ctx,
419431
machineScope.Cluster.Namespace,
@@ -428,6 +440,15 @@ func (r *PacketMachineReconciler) reconcile(ctx context.Context, machineScope *s
428440
return ctrl.Result{RequeueAfter: time.Second * 20}, nil
429441
}
430442
}
443+
case machineScope.PacketCluster.Spec.VIPManager == emlb.EMLBVIPID:
444+
if machineScope.IsControlPlane() {
445+
// Create new EMLB object
446+
lb := emlb.NewEMLB(r.PacketClient.GetConfig().DefaultHeader["X-Auth-Token"], machineScope.PacketCluster.Spec.ProjectID, machineScope.PacketCluster.Spec.Metro)
447+
448+
if err := lb.ReconcileVIPOrigin(ctx, machineScope, deviceAddr); err != nil {
449+
return ctrl.Result{}, err
450+
}
451+
}
431452
}
432453

433454
machineScope.SetReady()

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ require (
1010
github.com/pkg/errors v0.9.1
1111
github.com/spf13/cobra v1.8.0
1212
github.com/spf13/pflag v1.0.5
13+
github.com/stretchr/testify v1.9.0
14+
golang.org/x/oauth2 v0.18.0
1315
k8s.io/api v0.29.3
1416
k8s.io/apimachinery v0.29.3
1517
k8s.io/client-go v0.29.3
@@ -57,6 +59,7 @@ require (
5759
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
5860
github.com/modern-go/reflect2 v1.0.2 // indirect
5961
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
62+
github.com/pmezard/go-difflib v1.0.0 // indirect
6063
github.com/prometheus/client_golang v1.18.0 // indirect
6164
github.com/prometheus/client_model v0.5.0 // indirect
6265
github.com/prometheus/common v0.45.0 // indirect
@@ -73,7 +76,6 @@ require (
7376
golang.org/x/crypto v0.23.0 // indirect
7477
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
7578
golang.org/x/net v0.23.0 // indirect
76-
golang.org/x/oauth2 v0.18.0 // indirect
7779
golang.org/x/sync v0.6.0 // indirect
7880
golang.org/x/sys v0.20.0 // indirect
7981
golang.org/x/term v0.20.0 // indirect

0 commit comments

Comments
 (0)