WIP: Application load balancer controller#3
Conversation
|
@fischerman either mark the PR as "Draft" or use "WIP" as the title. The PR will then automatically marked as WIP/Draft |
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
dergeberl
left a comment
There was a problem hiding this comment.
This is my first batch of review comments. Only had a look into the /cmd/*, /pkg/controller/* (except the tests) and the *.md files, yet. I will continue with my review for the rest.
Maybe some comments are already resolved as you also pushed during my review (I did not crosscheck again, before submitting the comments now).
| | `alb.stackit.cloud/traget-pool-tls-skip-certificate-validation`| Boolean | IngressClass, Ingress, Service | Optional | Enables TLS bridging but skips certificate validation. | | ||
| | `alb.stackit.cloud/allowed-source-ranges`| String | IngressClass | Accepts a comma-separated list of IP ranges. E.g. 10.0.0.0/24,1.2.3.4/32. If unset, all IPs are allowed. | | ||
|
|
||
| ### Known Limitations |
There was a problem hiding this comment.
Maybe we add the NodePort requirement here? Also there are limits in the amount of resources (like listeners) which we should add here (or somewhere else).
There was a problem hiding this comment.
The limits like listeners, targetpools and path should also be documented. I remeber you having a list what the limits. We should also document how the this translate to ingress objects, like for the listeners it is the ports.
| ### Limits | ||
|
|
||
| The following limitations are imposed directly by the STACKIT ALB API (not the controller itself): | ||
| - Maximum targets per pool: An individual target pool can contain a maximum of 250 targets. |
|
|
||
| #### When to watch out for the listener limit | ||
|
|
||
| Because each IngressClass provisions a dedicated ALB instance, hitting the 20-listener threshold is rarely an issue for a basic setup but becomes a real risk when you start stacking custom ports across multiple applications sharing that same ALB. If your Ingress resources use the `alb.stackit.cloud/http-port` or `alb.stackit.cloud/https-port` annotations to expose different apps on unique custom port numbers, each distinctive port allocates its own listener on the shared ALB instance. This risk compounds quickly when those applications also require TLS encryption; since the controller must keep an extra HTTP listener active alongside the HTTPS listener to smoothly process automated ACME certificate challenges, a single secure app immediately consumes two slots instead of one, accelerating how fast you approach the API limit if multiple unique custom ports are configured. |
There was a problem hiding this comment.
This needs a rephrase. The ALB per ingress does not make it rarely to hit the limit (It is the default of 2 ports).
|
|
||
| A "target" in a pool corresponds directly to a worker node in your cluster. If you run a large cluster with a high number of worker nodes, or expect your cluster to dynamically scale to a large size, keep this limit in mind since a single backend Service port mapping cannot route traffic to more than 250 worker nodes simultaneously. | ||
|
|
||
| #### When to watch out for the listener limit |
There was a problem hiding this comment.
Also something for the Known Limitations.
|
|
||
| Because each IngressClass provisions a dedicated ALB instance, hitting the 20-listener threshold is rarely an issue for a basic setup but becomes a real risk when you start stacking custom ports across multiple applications sharing that same ALB. If your Ingress resources use the `alb.stackit.cloud/http-port` or `alb.stackit.cloud/https-port` annotations to expose different apps on unique custom port numbers, each distinctive port allocates its own listener on the shared ALB instance. This risk compounds quickly when those applications also require TLS encryption; since the controller must keep an extra HTTP listener active alongside the HTTPS listener to smoothly process automated ACME certificate challenges, a single secure app immediately consumes two slots instead of one, accelerating how fast you approach the API limit if multiple unique custom ports are configured. | ||
|
|
||
| ### Configuration |
There was a problem hiding this comment.
Please check that this are all, WAF seems to be missing
dergeberl
left a comment
There was a problem hiding this comment.
Comments for the stackit package and testutil
| return ctrl.Result{}, fmt.Errorf("failed to get load balancer: %w", err) | ||
| } | ||
|
|
||
| if *alb.Status != stackit.LBStatusReady { |
There was a problem hiding this comment.
In the latest SDK (v0.15.0) there are const for that LOADBALANCERSTATUS_STATUS_READY.
There was a problem hiding this comment.
There are some other changes that come with the update of the SDK. Can we postpone this to after the merge?
There was a problem hiding this comment.
Based in the changelog the new release only added the enums. The change should be minimal and only be a search and replace of the existing enums (as the prefix renamed). So not sure if it worth to postpone this.
https://github.com/stackitcloud/stackit-sdk-go/blob/main/services/alb/CHANGELOG.md
| type ProjectStatus string | ||
|
|
||
| const ( | ||
| LBStatusReady = "STATUS_READY" |
There was a problem hiding this comment.
Lets use the LOADBALANCERSTATUS_STATUS_READY from the SDK
| @@ -0,0 +1,22 @@ | |||
| package stackit | |||
There was a problem hiding this comment.
Can we also add the ResponseID to the error, to be able to trace errors
We do this also in cloud-provider-stackit for the LB api see:
https://github.com/stackitcloud/cloud-provider-stackit/blob/7a0ef7fa43f1d30f6ed4f88bfa2de1312c79b62f/pkg/stackit/client/loadbalancer.go#L43
https://github.com/stackitcloud/cloud-provider-stackit/blob/7a0ef7fa43f1d30f6ed4f88bfa2de1312c79b62f/pkg/stackit/client/helper.go#L12
| internalLB bool | ||
| externalIP string | ||
|
|
||
| listeners map[int16]*workTreeListener |
There was a problem hiding this comment.
int16 is not big enough for ports. It is later converted again to int32. Let's enable the gosec lint (this is how I found this error)
There was a problem hiding this comment.
I changed the type to uin16 and added out of range checks.
There was a problem hiding this comment.
lets also add gosec to the golangci-lint
There was a problem hiding this comment.
fc1c542 does not add the gosec to the golangci-lint config
|
|
||
| if *alb.Status != stackit.LBStatusReady { | ||
| // ALB is not yet ready, requeue | ||
| return ctrl.Result{RequeueAfter: 10 * time.Second}, nil |
There was a problem hiding this comment.
Should we write an event for this? In NLB controller we inform the user about this state via event
| hosts := []albsdk.HostConfig{} | ||
| for hostname, host := range listener.hosts { | ||
| paths := slices.Collect(maps.Values(host.paths)) | ||
| typeRank := map[networkingv1.PathType]int{ |
There was a problem hiding this comment.
this is static, can this live as a global variable?
| networkingv1.PathTypeImplementationSpecific: 2, | ||
| networkingv1.PathTypePrefix: 3, | ||
| } | ||
| slices.SortFunc(paths, func(a, b *workTreePath) int { |
There was a problem hiding this comment.
I'd like to have this sort function as a standalone instead of inlined in this for loop.
| Path: new("/healthz"), | ||
| OkStatuses: []string{"200"}, | ||
| }, | ||
| HealthyThreshold: new(int32(1)), |
There was a problem hiding this comment.
Should be constants for these defaults to easily find them in the code.
| }) | ||
| } | ||
|
|
||
| if len(listeners) == 0 { |
There was a problem hiding this comment.
This feels weird to me. If the load balancer does not yet contain any port, we shouldn't create it at all.
| DisableTargetSecurityGroupAssignment: new(true), // TODO: Make this configurable via flag. | ||
| Name: new(fmt.Sprintf("k8s-ingress-%s", t.ingressClass.UID)), | ||
| Labels: &map[string]string{ | ||
| "ingress-class-uid": string(t.ingressClass.UID), |
There was a problem hiding this comment.
Also from a user perspective it might be useful to add a cluster name label or something likewise here, so one can easily see which alb is for which cluster.
There was a problem hiding this comment.
file should be called reconcile.go or smth like that, has nothing to do with updating things.
Not sure in general why there are two files with the reconciler code.
| duplicateCerts := []string{} | ||
| for _, cert := range ingressClassCertificates { | ||
| if id, exists := certIDMap[spec.CertificateFingerprint(*cert.Data.FingerprintSha256)]; exists { | ||
| duplicateCerts = append(duplicateCerts, id) |
There was a problem hiding this comment.
Currently, we add the id from the certIDMap map which would result in deleting the certs that want to keep.
| duplicateCerts = append(duplicateCerts, id) | |
| duplicateCerts = append(duplicateCerts, cert.Id) |
|
|
||
| if ptr.Deref(c.Name, "") != ptr.Deref(d.Name, "") || | ||
| ptr.Deref(c.TargetPort, 0) != ptr.Deref(d.TargetPort, 0) || | ||
| len(c.Targets) != len(d.Targets) { |
There was a problem hiding this comment.
A len check is not enough here.
| return len(c.CertificateConfig.CertificateIds) != len(d.CertificateConfig.CertificateIds) | ||
| } | ||
|
|
||
| func targetPoolsChanged(current, desired []albsdk.TargetPool) bool { |
| } | ||
| for i := range ingressClassCertificates { | ||
| cert := &ingressClassCertificates[i] | ||
| if err := r.CertificateClient.DeleteCertificate(ctx, r.ALBConfig.Global.ProjectID, r.ALBConfig.Global.Region, *cert.Id); err != nil { |
There was a problem hiding this comment.
Is this an async call? If yes we should wait until certs are gone before removing the finalizer.
| region string, | ||
| ) *albsdk.CreateLoadBalancerPayload { | ||
| listeners := []albsdk.Listener{} | ||
| for port, listener := range t.listeners { |
There was a problem hiding this comment.
Map interactions are not deterministic. This will lead to issues while compare if a change is needed in
| CertificateIds: []string{}, | ||
| }, | ||
| } | ||
| for fingerprint, cert := range t.certificates { |
There was a problem hiding this comment.
Map interactions are not deterministic. If I remember correctly the order in the ALB matters, as always the first match will be used. This could lead in a flapping which cert is used if multiple are fitting.
| WithRule("my-host.local", WithPath("/", new(networkingv1.PathTypePrefix), service.Name, networkingv1.ServiceBackendPort{Number: 80})), | ||
| ) | ||
| testutil.CreateKubernetesResourceAndDeferDeletion(ctx, k8sClient, &ingress) | ||
|
|
There was a problem hiding this comment.
We also shpuld check that the IP is set in the status of the ingress.
| expected: true, | ||
| }, | ||
| { | ||
| name: "https certificates changed", |
There was a problem hiding this comment.
we should add the same but with also 2 Ids.
| expected: true, | ||
| }, | ||
| { | ||
| name: "target pool tls validation changed", |
There was a problem hiding this comment.
We should also add targets itself and test.
| @@ -0,0 +1,302 @@ | |||
| package ingress | |||
There was a problem hiding this comment.
In the update.go are multiple functions which also should be tested, like getServicesForIngresses, getTLSSecretsFromIngresses, getCertificatesForIngressClass. But also the logic for duplicateCerts should get tested.
| ) | ||
|
|
||
| Expect(errs).To(HaveLen(1)) | ||
| Expect(errs[0].Description).To(Equal("invalid certificate: tls: failed to find any PEM data in certificate input")) |
There was a problem hiding this comment.
Same as in test should return an error when the TLS secret isn't of type TLS
|
|
||
| tree := &WorkTreeALB{ | ||
| ingressClass: ingressClass, | ||
| planID: GetAnnotation(AnnotationPlanID, "", ingressClass), |
There was a problem hiding this comment.
Should we validate that we only get working PlanIDs. For NLBs we check that in cloud-provider-stackit
There was a problem hiding this comment.
Ties together with https://github.com/stackitcloud/application-load-balancer-controller/pull/3/changes#r3511950744
However, this does not only break an ingress but the entire ALB. Because we introduce the webhook I would just return an error here and not process any updates.
| @@ -0,0 +1,658 @@ | |||
| package spec | |||
There was a problem hiding this comment.
We should also add tests for:
theAnnotationHTTPSOnlyand that it not serves HTTP.the sorting based onAnnotationPriorityand creation timestamp.AnnotationPlanID- the handling when setting not allowed values in annotation (like no valid int/port, not valid cidr in source range)
GetUnusedCertificates
| @@ -0,0 +1,85 @@ | |||
| apiVersion: apps/v1 | |||
There was a problem hiding this comment.
We should align this with the deploy of CSI and CCM in cloud-provider-stackit (which are also not perfect, see stackitcloud/cloud-provider-stackit#1162).
For example:
- we can use also the kube-system namespace as this is also a system component like CSI and CCM
- the cloud config don't need to be a secret
- the mount path of the service account and cloud config
- the
apiEndpointsdon't need to be in the example (this is only internally needed) - lets also create a
/deployfolder instead if the/samples/deploy/ - we need to find a image tag which is working and don't need to be updated like
latest(not sure if this will be tagged on release)
There was a problem hiding this comment.
With #6 we get a latest tag for tagged versions. Lets use that in the deployment.
There was a problem hiding this comment.
I vote for latest tag here. We can leave it like it is for now and solve this directly before merging. After merge I will create a release which then also creates the latest tag
| ConditionNodeTermination corev1.NodeConditionType = "Terminating" | ||
| ) | ||
|
|
||
| func isNodeTerminating(node *corev1.Node) bool { |
There was a problem hiding this comment.
That changes also need to be added to the nodePredicate().
| if !httpsOnly && (httpPort <= 0 || httpPort > math.MaxUint16) { | ||
| errors = append(errors, ErrorEvent{ | ||
| Ingress: ingress, | ||
| Description: "HTTP port is out of range.", | ||
| }) | ||
| continue | ||
| } | ||
| if len(ingress.Spec.TLS) > 0 && (httpsPort <= 0 || httpsPort > math.MaxUint16) { | ||
| errors = append(errors, ErrorEvent{ | ||
| Ingress: ingress, | ||
| Description: "HTTPS port is out of range.", | ||
| }) | ||
| continue | ||
| } | ||
|
|
There was a problem hiding this comment.
We should add a test for that.
Avoid the use of underscores at the beginning of variable names
How to categorize this PR?
/kind enhancement
What this PR does / why we need it:
Implements the first alpha version of the STACKIT ALB controller.
Which issue(s) this PR fixes:
Fixes #
Special notes for your reviewer:
Breaking changes: