mirror of
https://github.com/ngrok/ngrok-operator.git
synced 2026-05-17 16:50:44 +00:00
3187082b0e
* docs: add RBAC overhaul design spec and requirements
Captures the motivation, constraints, and design decisions for the RBAC
overhaul before the implementation changes land.
* refactor(rbac): remove kubebuilder RBAC markers and disable controller-gen RBAC output
RBAC is now defined explicitly in Helm templates rather than generated
from code annotations. Removes all +kubebuilder:rbac markers from
controllers and drain.go, and drops the rbac output target from
controller-gen so it no longer clobbers the Helm-managed files.
* refactor(rbac): reorganize operator component RBAC into per-component Helm templates
Replaces the monolithic controller-rbac.yaml and per-component rbac.yaml
files with a consistent per-component directory structure (agent/,
api-manager/, bindings-forwarder/). Each component now owns its own
Role, RoleBinding, and optional namespace-scoped variants.
Key changes:
- agent: split rbac.yaml into role.yaml + rolebinding.yaml with
optional namespaced variants for namespace-scoped installs
- api-manager: moved from templates/rbac/role.yaml into dedicated
api-manager/ directory alongside its other templates; adds
leader-election-role.yaml and namespaced role support
- bindings-forwarder: renamed rbac.yaml -> role.yaml for consistency
- Deleted controller-rbac.yaml (replaced by api-manager/role.yaml)
- Renamed controller-{cm,deployment,pdb,serviceaccount}.yaml into
api-manager/ directory for cohesion
- Renamed service-account.yaml -> serviceaccount.yaml everywhere
- values.yaml/schema: adds crdAccessRoles and per-component RBAC flags
* feat(rbac): add CRD editor/viewer ClusterRoles for ngrok resources
Moves existing editor/viewer roles into a dedicated rbac/crd-access/
subdirectory with consistent naming, and adds new roles for
NgrokTrafficPolicy (previously missing).
These ClusterRoles are for users of the operator — granting cluster
members read or write access to ngrok CRDs — as opposed to the
operator's own service account permissions.
* test(rbac): update Helm unit tests and add chainsaw e2e RBAC verification
Updates all Helm unit tests and snapshots to match the reorganized
template structure (per-component directories, renamed files). Adds
new test suites for api-manager RBAC and crd-access roles.
Also adds a chainsaw e2e test that verifies the operator's service
accounts have exactly the permissions they need — no more, no less.
* chore: update generated artifacts after RBAC overhaul
Regenerates manifest-bundle.yaml and updates the Helm README to
reflect the new values added for per-component RBAC configuration.
* remove plan and chainsaw tests and make bindings not try to use watchNamespace
* break out k8soperator permissions and bindings permissions to separate role
* update requirements and gen manifest bundle
* make agent and api manager only query their release namespace when looking for the kubernetesoperator crd
* make bindings role always be created even if bindings is disabled
214 lines
7.2 KiB
Go
214 lines
7.2 KiB
Go
/*
|
|
MIT License
|
|
|
|
Copyright (c) 2024 ngrok, Inc.
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in all
|
|
copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
SOFTWARE.
|
|
*/
|
|
package gateway
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/go-logr/logr"
|
|
"k8s.io/apimachinery/pkg/api/meta"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/client-go/tools/events"
|
|
ctrl "sigs.k8s.io/controller-runtime"
|
|
"sigs.k8s.io/controller-runtime/pkg/builder"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
|
"sigs.k8s.io/controller-runtime/pkg/handler"
|
|
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
|
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
|
)
|
|
|
|
// GatewayClassReconciler reconciles a GatewayClass object
|
|
type GatewayClassReconciler struct {
|
|
client.Client
|
|
|
|
Log logr.Logger
|
|
Scheme *runtime.Scheme
|
|
Recorder events.EventRecorder
|
|
}
|
|
|
|
// SetupWithManager sets up the reconciler with the Manager
|
|
func (r *GatewayClassReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
|
return ctrl.NewControllerManagedBy(mgr).
|
|
For(&gatewayv1.GatewayClass{},
|
|
builder.WithPredicates(
|
|
predicate.NewPredicateFuncs(func(o client.Object) bool {
|
|
switch v := o.(type) {
|
|
case *gatewayv1.GatewayClass:
|
|
return ShouldHandleGatewayClass(v)
|
|
default:
|
|
}
|
|
r.Log.V(1).Info("Filtering out object", "object", o)
|
|
return false
|
|
}),
|
|
),
|
|
).
|
|
Watches(
|
|
&gatewayv1.Gateway{},
|
|
handler.EnqueueRequestsFromMapFunc(r.findGatewayClassForGateway),
|
|
).
|
|
// WithEventFilter filters out events. It applies to all events, including those from watches.
|
|
WithEventFilter(
|
|
predicate.Or(
|
|
predicate.GenerationChangedPredicate{},
|
|
),
|
|
).
|
|
Complete(r)
|
|
}
|
|
|
|
// Reconcile reconciles a GatewayClass object ctrl.Request
|
|
func (r *GatewayClassReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
|
log := r.Log.WithValues("gatewayclass", req.NamespacedName)
|
|
ctrl.LoggerInto(ctx, log)
|
|
|
|
gwc := &gatewayv1.GatewayClass{}
|
|
if err := r.Get(ctx, req.NamespacedName, gwc); err != nil {
|
|
return ctrl.Result{}, client.IgnoreNotFound(err)
|
|
}
|
|
|
|
if !ShouldHandleGatewayClass(gwc) {
|
|
return ctrl.Result{}, nil
|
|
}
|
|
|
|
log.V(1).Info("Reconciling GatewayClass")
|
|
|
|
// Accept the GatewayClass if it is not already accepted
|
|
if err := r.reconcileAcceptedCondition(ctx, gwc); err != nil {
|
|
return ctrl.Result{}, err
|
|
}
|
|
|
|
return ctrl.Result{}, r.reconcileGatewayExistsFinalizer(ctx, gwc)
|
|
}
|
|
|
|
// reconcileAcceptedCondition makes sure that the GatewayClass has an accepted condition
|
|
func (r *GatewayClassReconciler) reconcileAcceptedCondition(ctx context.Context, gwc *gatewayv1.GatewayClass) error {
|
|
log := ctrl.LoggerFrom(ctx)
|
|
|
|
if gatewayClassIsAccepted(gwc) {
|
|
log.V(3).Info("GatewayClass already accepted")
|
|
return nil
|
|
}
|
|
|
|
changed := meta.SetStatusCondition(&gwc.Status.Conditions, metav1.Condition{
|
|
Type: string(gatewayv1.GatewayClassConditionStatusAccepted),
|
|
Status: metav1.ConditionTrue,
|
|
Reason: string(gatewayv1.GatewayClassReasonAccepted),
|
|
Message: "gatewayclass accepted by the ngrok controller",
|
|
ObservedGeneration: gwc.Generation,
|
|
})
|
|
|
|
if !changed {
|
|
return nil
|
|
}
|
|
|
|
log.V(1).Info("Accepting GatewayClass")
|
|
return r.Status().Update(ctx, gwc)
|
|
}
|
|
|
|
// reconcileGatewayExistsFinalizer adds the GatewayClassGatewayExistsFinalizer if there are gateways that reference this GatewayClass.
|
|
// It removes the finalizer if there are no gateways that reference this GatewayClass.
|
|
func (r *GatewayClassReconciler) reconcileGatewayExistsFinalizer(ctx context.Context, gwc *gatewayv1.GatewayClass) error {
|
|
log := ctrl.LoggerFrom(ctx)
|
|
|
|
// Filter out gateways that are not of this GatewayClass
|
|
log.V(3).Info("Finding gateways for GatewayClass", "gatewayclass", gwc.Name)
|
|
gatewayList := &gatewayv1.GatewayList{}
|
|
if err := r.List(ctx, gatewayList); err != nil {
|
|
return err
|
|
}
|
|
|
|
filtered := []gatewayv1.Gateway{}
|
|
for _, gw := range gatewayList.Items {
|
|
if string(gw.Spec.GatewayClassName) == gwc.Name {
|
|
filtered = append(filtered, gw)
|
|
}
|
|
}
|
|
|
|
log.V(3).Info("Filtered gateways for GatewayClass", "matching", filtered)
|
|
|
|
if len(filtered) == 0 {
|
|
if controllerutil.ContainsFinalizer(gwc, gatewayv1.GatewayClassFinalizerGatewaysExist) {
|
|
log.V(1).Info("Removing finalizer", "finalizer", gatewayv1.GatewayClassFinalizerGatewaysExist)
|
|
|
|
patch := client.MergeFrom(gwc.DeepCopy())
|
|
controllerutil.RemoveFinalizer(gwc, gatewayv1.GatewayClassFinalizerGatewaysExist)
|
|
return r.Patch(ctx, gwc, patch)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
if !controllerutil.ContainsFinalizer(gwc, gatewayv1.GatewayClassFinalizerGatewaysExist) {
|
|
log.V(1).Info("Adding finalizer", "finalizer", gatewayv1.GatewayClassFinalizerGatewaysExist)
|
|
|
|
patch := client.MergeFrom(gwc.DeepCopy())
|
|
controllerutil.AddFinalizer(gwc, gatewayv1.GatewayClassFinalizerGatewaysExist)
|
|
return r.Patch(ctx, gwc, patch)
|
|
}
|
|
|
|
log.V(1).Info("Finalizers match expected state")
|
|
return nil
|
|
}
|
|
|
|
// findGatewayClassForGateway returns a reconcile.Request for the GatewayClass of the given gateway. It is used by
|
|
// the watch on Gateway objects to trigger a reconciliation of the GatewayClass.
|
|
func (r *GatewayClassReconciler) findGatewayClassForGateway(_ context.Context, o client.Object) []reconcile.Request {
|
|
log := r.Log
|
|
|
|
gw, ok := o.(*gatewayv1.Gateway)
|
|
if !ok {
|
|
log.Error(nil, "object is not a Gateway", "object", o)
|
|
return nil
|
|
}
|
|
|
|
log = log.WithValues("gateway.name", gw.Name, "gateway.namespace", gw.Namespace, "gateway.gatewayClassName", gw.Spec.GatewayClassName)
|
|
|
|
if gw.Spec.GatewayClassName == "" {
|
|
log.V(1).Info("Gateway does not have a GatewayClassName, ignoring")
|
|
return nil
|
|
}
|
|
|
|
log.V(1).Info("Enqueueing request for gatewayclass")
|
|
return []reconcile.Request{
|
|
{
|
|
NamespacedName: client.ObjectKey{
|
|
Namespace: "",
|
|
Name: string(gw.Spec.GatewayClassName),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// ShouldHandleGatewayClass returns true if the GatewayClass should be handled by this controller
|
|
// based on the ControllerName field, false otherwise.
|
|
func ShouldHandleGatewayClass(gatewayClass *gatewayv1.GatewayClass) bool {
|
|
return gatewayClass.Spec.ControllerName == ControllerName
|
|
}
|
|
|
|
func gatewayClassIsAccepted(gwc *gatewayv1.GatewayClass) bool {
|
|
return meta.IsStatusConditionTrue(gwc.Status.Conditions, string(gatewayv1.GatewayClassConditionStatusAccepted))
|
|
}
|