Files
Aleksei Sviridkin ad3f15d4f3 feat(gateway)!: reserve a passthrough listener's name by the spec alone
The TenantGateway controller withheld the HTTPS-terminate listener for a
name a passthrough listener answers only once a TLSRoute was attached
there and resolved a backend, mirroring how Cilium builds its filter
chains. That kept a served endpoint up on Cilium 1.19.5, but it tied
every route's status to one implementation's translator and needed
Service and ReferenceGrant watches to land the deferred decision.

The shipped operator accepts a wildcard terminate listener beside a
narrower passthrough listener on 443 and keeps conflicting every other
overlap. A terminate listener for the passthrough name itself is not
narrower, so on 443 the rule now protects the dataplane outright; on a
native port Gateway API would serve the pair, and the rule holds as a
policy that keeps a platform name with its passthrough listener. Reading
it off the spec makes the outcome the same on every implementation.

Under http01 an HTTPRoute claiming such a name gets no listener and no
certificate whether or not a TLSRoute is attached. A route whose every
hostname is reserved reads Accepted=False with
NoMatchingListenerHostname; one that also claims a free hostname stays
Accepted and is served there, as Gateway API accepts a route whose
hostnames intersect at least one listener. dns01 and existingSecret are
unchanged: their wildcard listener still covers such a name, and the
passthrough listener answers it by its exact server name.

The chart writes the passthrough services on the publishing tenant
alone, so the names reserved by default are the ones the platform
publishes there through its own TLSRoutes. The shape an upgrade
changes is an HTTPRoute on such a name whose platform TLSRoute forwards
nowhere, because it never attached (the package owning the service is
not installed) or because none of its backends resolve: that route kept
a terminate listener and now gets none.

The backend resolution, its watches and the RBAC rules that served them
go away.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-10-10 06:32:42 +03:00

107 lines
3.4 KiB
Go

/*
Copyright 2026 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package tenantgateway
import (
"context"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
gatewayv1alpha1 "github.com/cozystack/cozystack/api/gateway/v1alpha1"
)
// routeToTenantGateway returns an EventHandler that maps an HTTPRoute
// or TLSRoute change back to the TenantGateway resources whose Gateway
// the route attaches to. controller-runtime requeues the parent so
// listener / cert lifecycle stays in sync with route additions and
// removals.
func (r *Reconciler) routeToTenantGateway() handler.EventHandler {
return handler.EnqueueRequestsFromMapFunc(r.mapRouteToTenantGateways)
}
// mapRouteToTenantGateways is the underlying MapFunc, exposed as a
// method so tests can drive it directly without going through
// controller-runtime's EventHandler.Generic().
func (r *Reconciler) mapRouteToTenantGateways(ctx context.Context, obj client.Object) []reconcile.Request {
var (
parentRefs []gatewayv1.ParentReference
routeNs string
)
switch route := obj.(type) {
case *gatewayv1.HTTPRoute:
parentRefs = route.Spec.ParentRefs
routeNs = route.Namespace
case *gatewayv1alpha2.TLSRoute:
parentRefs = route.Spec.ParentRefs
routeNs = route.Namespace
default:
return nil
}
if len(parentRefs) == 0 {
return nil
}
list := &gatewayv1alpha1.TenantGatewayList{}
if err := r.List(ctx, list); err != nil {
log.FromContext(ctx).Error(err, "list TenantGateways for route mapper")
return nil
}
var out []reconcile.Request
for i := range list.Items {
tgw := &list.Items[i]
if _, ok := pickAttachingParentRef(parentRefs, routeNs, tgw); !ok {
continue
}
out = append(out, reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: tgw.Namespace,
Name: tgw.Name,
},
})
}
return out
}
// mapGatewayClassToTenantGateways requeues the TenantGateways on a
// class, because GatewayClassUnsupported is judged from the class's
// status.supportedFeatures, which a class writes after it is accepted
// and may change on an upgrade of its controller, with nothing on the
// TenantGateway changing.
func (r *Reconciler) mapGatewayClassToTenantGateways(ctx context.Context, obj client.Object) []reconcile.Request {
list := &gatewayv1alpha1.TenantGatewayList{}
if err := r.List(ctx, list); err != nil {
log.FromContext(ctx).Error(err, "list TenantGateways for GatewayClass mapper")
return nil
}
var out []reconcile.Request
for i := range list.Items {
tgw := &list.Items[i]
if gatewayClassName(tgw) != obj.GetName() {
continue
}
out = append(out, reconcile.Request{NamespacedName: types.NamespacedName{Namespace: tgw.Namespace, Name: tgw.Name}})
}
return out
}