Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions internal/controller/istio_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type IstioReconciler struct {
// +kubebuilder:rbac:groups=operator.tigera.io,resources=istios,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=operator.tigera.io,resources=istios/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=operator.tigera.io,resources=istios/finalizers,verbs=update
// +kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gateways,verbs=get;list;watch

// SetupWithManager sets up the controller with the Manager.
func (r *IstioReconciler) SetupWithManager(mgr ctrl.Manager, opts options.ControllerOptions) error {
Expand Down
5 changes: 5 additions & 0 deletions pkg/controller/istio/istio_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
v3 "github.com/tigera/api/pkg/apis/projectcalico/v3"
"github.com/tigera/api/pkg/lib/numorstring"
operatorv1 "github.com/tigera/operator/api/v1"
"github.com/tigera/operator/pkg/controller/istio/waypoint"
"github.com/tigera/operator/pkg/controller/options"
"github.com/tigera/operator/pkg/controller/status"
"github.com/tigera/operator/pkg/controller/utils"
Expand Down Expand Up @@ -95,6 +96,10 @@ func Add(mgr manager.Manager, opts options.ControllerOptions) error {
return fmt.Errorf("istio-controller failed to create periodic reconcile watch: %w", err)
}

if err := waypoint.Add(mgr, opts); err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, a bit of a new pattern here (I think all the other "sub" controllers are just added in the same place as main controllers).

Not necessarily against this, though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it was just easier for me to understand this format, but also happy to go with the established pattern. Let me know what your preference would be as a maintainer 😅

return fmt.Errorf("failed to add waypoint pull secrets controller: %w", err)
}

return nil
}

Expand Down
202 changes: 202 additions & 0 deletions pkg/controller/istio/waypoint/waypoint_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
// Copyright (c) 2026 Tigera, Inc. All rights reserved.

// 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 waypoint

import (
"context"
"fmt"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/handler"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
gapi "sigs.k8s.io/gateway-api/apis/v1"

operatorv1 "github.com/tigera/operator/api/v1"
"github.com/tigera/operator/pkg/controller/options"
"github.com/tigera/operator/pkg/controller/utils"
"github.com/tigera/operator/pkg/ctrlruntime"
"github.com/tigera/operator/pkg/render"
"github.com/tigera/operator/pkg/render/common/secret"
)

const (
// IstioWaypointClassName is the GatewayClass name used by Istio waypoints.
IstioWaypointClassName = "istio-waypoint"

// WaypointPullSecretLabel labels secrets copied by this controller. We use a label rather
// than owner references because the controller needs to efficiently find and clean up its
// managed secrets during reconciliation — for example, when pull secrets are removed from
// Installation or when the Istio CR is deleted. A label selector provides a simple,
// cross-namespace query that covers all cleanup scenarios, whereas owner references would
// only automate Gateway-deletion cleanup via Kubernetes garbage collection.
WaypointPullSecretLabel = "operator.tigera.io/istio-waypoint-pull-secret"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious why a label is the right call here instead of using owner references? If there's a reason it would be good to expand on that in the comment.

Copy link
Member Author

@electricjesus electricjesus Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went with labels here since we need to find and clean up these secrets across namespaces; not just on Gateway deletion, but also when pull secrets get removed from Installation or the Istio CR gets deleted. MatchingLabels makes that easy. Owner refs would only cover the Gateway-deletion case via GC, and we'd still need some way to track secrets for the other cleanup paths. Similar to how ESGatewaySelectorLabel works in the logstorage controller.

)

var log = logf.Log.WithName("controller_istio_waypoint")

// Add creates the waypoint pull secrets controller and adds it to the Manager.
func Add(mgr manager.Manager, opts options.ControllerOptions) error {
if !opts.EnterpriseCRDExists {
return nil
}

r := &ReconcileWaypointSecrets{
Client: mgr.GetClient(),
scheme: mgr.GetScheme(),
}

c, err := ctrlruntime.NewController("istio-waypoint-secrets-controller", mgr, controller.Options{Reconciler: r})
if err != nil {
return fmt.Errorf("failed to create istio-waypoint-secrets-controller: %w", err)
}

// Watch Gateway resources, filtering for istio-waypoint class only.
err = c.WatchObject(&gapi.Gateway{}, &handler.EnqueueRequestForObject{}, predicate.Funcs{
CreateFunc: func(e event.CreateEvent) bool {
gw, ok := e.Object.(*gapi.Gateway)
return ok && string(gw.Spec.GatewayClassName) == IstioWaypointClassName
},
UpdateFunc: func(e event.UpdateEvent) bool {
gw, ok := e.ObjectNew.(*gapi.Gateway)
return ok && string(gw.Spec.GatewayClassName) == IstioWaypointClassName
},
DeleteFunc: func(e event.DeleteEvent) bool {
gw, ok := e.Object.(*gapi.Gateway)
return ok && string(gw.Spec.GatewayClassName) == IstioWaypointClassName
},
GenericFunc: func(e event.GenericEvent) bool {
gw, ok := e.Object.(*gapi.Gateway)
return ok && string(gw.Spec.GatewayClassName) == IstioWaypointClassName
},
})
if err != nil {
return fmt.Errorf("istio-waypoint-secrets-controller failed to watch Gateway resource: %w", err)
}

// Watch Istio CR for pull secret config changes.
err = c.WatchObject(&operatorv1.Istio{}, &handler.EnqueueRequestForObject{})
if err != nil {
return fmt.Errorf("istio-waypoint-secrets-controller failed to watch Istio resource: %w", err)
}

// Watch Installation for pull secret changes.
if err = utils.AddInstallationWatch(c); err != nil {
return fmt.Errorf("istio-waypoint-secrets-controller failed to watch Installation resource: %w", err)
}

// Periodic reconcile as a backstop.
if err = utils.AddPeriodicReconcile(c, utils.PeriodicReconcileTime, &handler.EnqueueRequestForObject{}); err != nil {
return fmt.Errorf("istio-waypoint-secrets-controller failed to create periodic reconcile watch: %w", err)
}

return nil
}

// ReconcileWaypointSecrets copies pull secrets to namespaces that contain
// istio-waypoint Gateways so that waypoint pods can pull images from private registries.
type ReconcileWaypointSecrets struct {
client.Client
scheme *runtime.Scheme
}

func (r *ReconcileWaypointSecrets) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) {
reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
reqLogger.V(1).Info("Reconciling waypoint pull secrets")

// Determine which secrets need to exist (toCreate) based on current state,
// and which existing secrets are stale (toDelete).
var toCreate []client.Object
var toDelete []client.Object

// Get the Istio CR - if not found or being deleted, all existing secrets are stale.
instance := &operatorv1.Istio{}
err := r.Get(ctx, utils.DefaultInstanceKey, instance)
istioActive := err == nil && instance.DeletionTimestamp.IsZero()
if err != nil && !errors.IsNotFound(err) {
return reconcile.Result{}, err
}

// Build the desired set of secrets if Istio is active.
targetNamespaces := map[string]bool{}
if istioActive {
_, installation, err := utils.GetInstallation(ctx, r)
if err != nil {
if errors.IsNotFound(err) {
reqLogger.V(1).Info("Installation not found")
return reconcile.Result{}, nil
}
return reconcile.Result{}, err
}

pullSecrets, err := utils.GetNetworkingPullSecrets(installation, r)
if err != nil {
return reconcile.Result{}, err
}

// List all Gateway resources and filter for istio-waypoint class.
gatewayList := &gapi.GatewayList{}
if err := r.List(ctx, gatewayList); err != nil {
return reconcile.Result{}, fmt.Errorf("failed to list Gateways: %w", err)
}

for i := range gatewayList.Items {
gw := &gatewayList.Items[i]
if string(gw.Spec.GatewayClassName) == IstioWaypointClassName {
targetNamespaces[gw.Namespace] = true
}
}

// Build desired secrets for each target namespace.
for ns := range targetNamespaces {
copied := secret.CopyToNamespace(ns, pullSecrets...)
for _, s := range copied {
if s.Labels == nil {
s.Labels = map[string]string{}
}
s.Labels[WaypointPullSecretLabel] = "true"
toCreate = append(toCreate, s)
}
}
}

// List all existing secrets managed by this controller and mark stale ones for deletion.
existingSecrets := &corev1.SecretList{}
if err := r.List(ctx, existingSecrets, client.MatchingLabels{WaypointPullSecretLabel: "true"}); err != nil {
return reconcile.Result{}, fmt.Errorf("failed to list waypoint pull secrets: %w", err)
}
for i := range existingSecrets.Items {
s := &existingSecrets.Items[i]
if !targetNamespaces[s.Namespace] {
toDelete = append(toDelete, s)
}
}

// Use a single passthrough component to handle both creation and deletion.
hdlr := utils.NewComponentHandler(log, r, r.scheme, nil)
component := render.NewPassthrough(toCreate, toDelete)
if err := hdlr.CreateOrUpdateOrDelete(ctx, component, nil); err != nil {
return reconcile.Result{}, fmt.Errorf("failed to reconcile waypoint pull secrets: %w", err)
}

return reconcile.Result{}, nil
}
Loading
Loading