Files
cozystack/internal/controller/serviceexposure/backend/externalips.go
Aleksei Sviridkin 664704d7b6 feat(controller): reconcile ServiceExposure into backend LB resources
Add the serviceexposure controller to the cozystack-controller binary. It
resolves a ServiceExposure's ExposureClass to a backend and renders the
backend-specific pool and announcer CRs, then reports the assigned IPs in
status.

Backends are pluggable behind a small interface, one per file:

- externalIPs: renders nothing (the node owns the address) — preserves the
  historical default behaviour.
- metallb: IPAddressPool + L2Advertisement in cozy-metallb, scoped to the
  target Service's namespace.
- cilium: CiliumLoadBalancerIPPool (cilium.io/v2) + CiliumL2AnnouncementPolicy
  (cilium.io/v2alpha1), scoped to the Service via serviceSelector.
- robotlb: renders nothing (the cloud LB owns allocation and announcement).

The rendered CRs belong to foreign API groups not in the Go scheme, so they
are emitted as unstructured objects. They carry no OwnerReference (the
Cilium CRs are cluster-scoped and the MetalLB CRs live in a different
namespace than the owning ServiceExposure), so ownership is tracked via a
management label plus owner annotations, cleanup is driven by a finalizer,
and the controller refuses to take over an identically-named resource it
does not manage. The controller never mutates the target Service, leaving
its shape to the chart that renders it.

Grant the controller write access to metallb.io and cilium.io pool /
announcer resources.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-06-25 20:47:19 +03:00

54 lines
1.8 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 backend
import (
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
networkv1alpha1 "github.com/cozystack/cozystack/api/network/v1alpha1"
)
// externalIPsBackend pins the Service to operator-supplied addresses via
// Service.spec.externalIPs. The node already owns the address, so there
// is no pool to allocate and no announcer to render — this backend
// produces zero cluster resources and exists to preserve the historical
// default behaviour while keeping the API uniform across backends. The
// Service's own spec.externalIPs is set by the chart that renders it, not
// by this controller.
type externalIPsBackend struct{}
func (externalIPsBackend) Name() networkv1alpha1.ExposureBackend {
return networkv1alpha1.BackendExternalIPs
}
func (externalIPsBackend) Desired(
_ *networkv1alpha1.ExposureClass,
_ []string,
) ([]*unstructured.Unstructured, error) {
return nil, nil
}
func (externalIPsBackend) Observe(svc *corev1.Service) (assignedIPs []string, ready bool, reason string) {
if len(svc.Spec.ExternalIPs) > 0 {
return append([]string(nil), svc.Spec.ExternalIPs...), true, ""
}
return nil, false, "NoExternalIPs"
}
func init() { register(externalIPsBackend{}) }