Conversation
…ntroller The StackitCluster reconciler now owns the whole apiserver target pool and derives it from the control-plane Machines of the cluster.
A cluster deletion filters out every machine at once, and the placeholder must not overwrite the pool while the API server is still needed.
| // maxTargetNameLength is the STACKIT limit on a target display name. | ||
| maxTargetNameLength = 63 |
There was a problem hiding this comment.
thats the same length as kubernetes resources anyway
https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names
| // targetName turns a machine name into a valid STACKIT target display name: | ||
| // letters, digits and inner hyphens, at most 63 characters. A qualifying name is | ||
| // returned unchanged; any other carries a digest so that two machines cannot | ||
| // collapse onto one target. | ||
| func targetName(machineName string) string { | ||
| var sanitized strings.Builder | ||
| previousHyphen := false | ||
| for _, r := range machineName { | ||
| switch { | ||
| case (r >= '0' && r <= '9') || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z'): | ||
| sanitized.WriteRune(r) | ||
| previousHyphen = false | ||
| case !previousHyphen: | ||
| sanitized.WriteByte('-') | ||
| previousHyphen = true | ||
| } | ||
| } | ||
| name := strings.Trim(sanitized.String(), "-") | ||
| if name == machineName && len(name) <= maxTargetNameLength { | ||
| return name | ||
| } | ||
|
|
||
| sum := sha256.Sum256([]byte(machineName)) | ||
| digest := hex.EncodeToString(sum[:])[:targetNameDigestLength] | ||
| if name == "" { | ||
| return digest | ||
| } | ||
| suffix := "-" + digest | ||
| if len(name) > maxTargetNameLength-len(suffix) { | ||
| name = strings.TrimRight(name[:maxTargetNameLength-len(suffix)], "-") | ||
| } | ||
| return name + suffix |
There was a problem hiding this comment.
as kubernetes machine names are forced to follow RFC1123 as mentioned above. capital letters or whitespace isn't possible anyways. so why do we need this sanitization step?
| // Control plane status updates are frequent, and each one reaches this path. | ||
| if targetPool.GetTargetPort() == port && sameTargets(targetPool.GetTargets(), desired) { |
There was a problem hiding this comment.
what is this comment supposed to tell me? so yes this happens frequently but why do i need to know this?
| // STACKIT NLB target pools must contain at least one target. Leave the | ||
| // last target in place; deleting the load balancer removes it. | ||
| return nil | ||
| keys = append(keys, target.GetDisplayName()+"\x00"+target.GetIp()) |
There was a problem hiding this comment.
So whats the "magic" nullbyte for? This requires a comment or documentation link or something
| Watches(&clusterv1.Cluster{}, handler.EnqueueRequestsFromMapFunc(r.stackitClusterRequestsForCluster)). | ||
| Watches( | ||
| &clusterv1.Machine{}, | ||
| handler.EnqueueRequestsFromMapFunc(r.stackitClusterRequestsForMachine), | ||
| builder.WithPredicates(predicate.Funcs{UpdateFunc: machineTargetPoolChanged}), | ||
| ). |
There was a problem hiding this comment.
This naming scheme seems a bit off lets take the opportunity to get something less verbose in place:
| Watches(&clusterv1.Cluster{}, handler.EnqueueRequestsFromMapFunc(r.stackitClusterRequestsForCluster)). | |
| Watches( | |
| &clusterv1.Machine{}, | |
| handler.EnqueueRequestsFromMapFunc(r.stackitClusterRequestsForMachine), | |
| builder.WithPredicates(predicate.Funcs{UpdateFunc: machineTargetPoolChanged}), | |
| ). | |
| Watches(&clusterv1.Cluster{}, handler.EnqueueRequestsFromMapFunc(r.FilterForStackitClusterResources)). | |
| Watches( | |
| &clusterv1.Machine{}, | |
| handler.EnqueueRequestsFromMapFunc(r.LoadBalancerTargetsForControlPlanes), | |
| builder.WithPredicates(predicate.Funcs{UpdateFunc: machineTargetPoolChanged}), | |
| ). |
As the comment of stackitClusterRequestsForMachine already implies its only concerned about the API servers load balancer for the control planes so we should indicate this in the name. I would suggest:
LoadBalancerTargetsForControlPlanes
And while we are at it: stackitClusterRequestsForCluster -> FilterForStackitClusterResources
Closes #24
The API server load balancer is created by the
StackitClusterreconciler while its targetpool was maintained by
StackitMachine. That leaked cluster-level infrastructure into themachine controller and let several machines write to the same pool concurrently. STACKIT takes
an explicit target list rather than label-based membership, so the pool now has exactly one
owner.
StackitClusterwatches control-planeMachineevents and rebuilds the pool fromMachine.Status.Addresses, which Cluster API propagates fromStackitMachine.status.addresses.No RBAC change. The watch carries a predicate, since control plane status updates are frequent
and each one would otherwise cost a full reconcile.
EnsureAPIServerLoadBalancerTargetandDeleteAPIServerLoadBalancerTargetare replaced bySetAPIServerLoadBalancerTargets, which applies the pool as a set and skips the write whennothing changed.
reconcileAPIServerLoadBalancerTarget,deleteAPIServerLoadBalancerTargetand
EnsureForMachineare gone.retryable. Machine names are therefore sanitized into valid display names, with a digest
against collisions, and duplicate target IPs are dropped.
plane before the infrastructure, so every machine is filtered out at once while the API server
is still needed for drain and etcd member removal.
sets only
LoadBalancerReady, neverReady. The machine reconciler stops while the cluster isnot ready, so failing the cluster would block replacing the machine whose target is broken.
Accepted trade-off: removing a target is no longer a precondition of the machine finalizer.
If the cluster controller is wedged, a target pointing at a dead IP survives, and the create path
sets no
ActiveHealthCheckthat would fail it out. Should STACKIT recycle that IP, the loadbalancer forwards API traffic to an unrelated VM.
make lint,make testandmake manifestsare clean,config/is unchanged. The specs for thepool collapse and the permanent-failure case were checked against the unfixed code and fail there.
E2E was not run.