Authenticate with a seeded operator key; fold escalation and switches into TerdutTeam
Credentials: the TerdutServer controller generates <name>-operator-key in the server's own namespace (owned by it) and hands it to the pods as TERDUT_OPERATOR_KEY; the server creates its instance-scoped account from it at every start. A replaced Secret rolls the pods. The bootstrap handshake, the checkpoint Secret, per-team service accounts and credentials Secrets, BootstrapStateLost and credentials.deletionPolicy are gone. CRDs: TerdutServer, TerdutTeam and TerdutAlertSource. TerdutEscalationRule and TerdutDeadmanSwitch become spec.escalation and spec.deadmanSwitches[] on the team (matched by name, extras removed); team invites are removed. A team is created under the identity <namespace>/<name> (external_id), so a retry, a lost status or a deleted team heal by repeating the same call, and a display name owned by another team is TeamNameTaken instead of an adoption. The server resolves escalation usernames (UnknownUser condition). OIDC claim names and trustEmail are spec fields. Fixes: query values are URL-escaped; every delete treats 404 as success; deleting a team no longer depends on allowedTeams consent; a switch or integration deleted on the server is recreated; unnamed switches take the CR's name. Cleanup: scaffold e2e test, AGENTS.md, devcontainer, unused config/ pieces and Client.Version() removed; DESIGN.md, README, ROADMAP and the demo (run-demo.sh, manifests) rewritten for the new design. Secret RBAC stays cluster-wide, now stated in DESIGN.md section 9. Claude-Session: https://claude.ai/code/session_016mBLURvJoMuUEr9cB2RpUN
This commit is contained in:
@@ -5,7 +5,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
@@ -15,33 +14,25 @@ import (
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
"sigs.k8s.io/controller-runtime/pkg/recorder"
|
||||
|
||||
terdutv1alpha1 "git.ryuvia.com/niklas/terdut-operator/api/v1alpha1"
|
||||
"git.ryuvia.com/niklas/terdut-operator/internal/tdclient"
|
||||
)
|
||||
|
||||
// teamFinalizerName cleans up the team-scoped credentials Secret this
|
||||
// controller generates in the operator's own namespace on delete.
|
||||
// teamFinalizerName deletes the team on the server before the CR goes.
|
||||
const teamFinalizerName = "terdut.ryuvia.com/terdutteam"
|
||||
|
||||
// TerdutTeamReconciler reconciles a TerdutTeam object.
|
||||
//
|
||||
// Every child resolves its own teamRef/serverRef independently and never
|
||||
// chains up through another controller (DESIGN.md §5) — this one talks
|
||||
// directly to the TerdutServer it references and to terdut-server's API,
|
||||
// never to TerdutServerReconciler.
|
||||
// TerdutTeamReconciler reconciles a TerdutTeam: the team itself plus its
|
||||
// escalation ladder and dead man's switches, all through the referenced
|
||||
// TerdutServer's operator key.
|
||||
type TerdutTeamReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
|
||||
// OperatorNamespace is where every credentials Secret this controller
|
||||
// reads (the referenced TerdutServer's) or writes (this team's own)
|
||||
// lives (DESIGN.md §6) — never a TerdutTeam's or TerdutServer's own
|
||||
// namespace.
|
||||
OperatorNamespace string
|
||||
|
||||
Recorder recorder.EventRecorder
|
||||
NewClient func(endpoint string) *tdclient.Client
|
||||
}
|
||||
@@ -50,10 +41,16 @@ type TerdutTeamReconciler struct {
|
||||
// +kubebuilder:rbac:groups=terdut.ryuvia.com,resources=terdutteams/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=terdut.ryuvia.com,resources=terdutteams/finalizers,verbs=update
|
||||
// +kubebuilder:rbac:groups=terdut.ryuvia.com,resources=terdutservers,verbs=get;list;watch
|
||||
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
|
||||
// +kubebuilder:rbac:groups="",resources=namespaces,verbs=get;list;watch
|
||||
// +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
|
||||
|
||||
// teamExternalID is the identity this CR gives its team on the server, so the
|
||||
// team is found by who owns it and not by its (editable, global) display name.
|
||||
func teamExternalID(team *terdutv1alpha1.TerdutTeam) string {
|
||||
return team.Namespace + "/" + team.Name
|
||||
}
|
||||
|
||||
func (r *TerdutTeamReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
log := logf.FromContext(ctx)
|
||||
|
||||
@@ -79,67 +76,56 @@ func (r *TerdutTeamReconciler) Reconcile(ctx context.Context, req ctrl.Request)
|
||||
|
||||
srv, resolveErr := r.resolveServerRef(ctx, &team)
|
||||
if resolveErr != nil {
|
||||
return r.setTeamNotReady(ctx, &team, resolveErr.reason, resolveErr.message, waitInterval)
|
||||
return r.setTeamNotReady(ctx, &team, resolveErr.reason, resolveErr.message)
|
||||
}
|
||||
if srv.Status.CredentialsSecretRef == nil {
|
||||
if !meta.IsStatusConditionTrue(srv.Status.Conditions, terdutv1alpha1.ConditionReady) {
|
||||
return r.setTeamNotReady(ctx, &team, terdutv1alpha1.ReasonWaitingForServer,
|
||||
fmt.Sprintf("TerdutServer %q is not Bootstrapped yet", srv.Name), waitInterval)
|
||||
fmt.Sprintf("TerdutServer %q is not Ready yet", srv.Name))
|
||||
}
|
||||
|
||||
newClient := r.NewClient
|
||||
if newClient == nil {
|
||||
newClient = tdclient.New
|
||||
}
|
||||
|
||||
instanceKey, err := readOperatorSecret(ctx, r.Client, r.OperatorNamespace, srv.Status.CredentialsSecretRef)
|
||||
tc, err := r.serverClient(ctx, srv)
|
||||
if err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
endpoint := serviceURL(srv)
|
||||
team.Status.ServerEndpoint = endpoint
|
||||
instanceClient := newClient(endpoint).WithToken(instanceKey)
|
||||
|
||||
if team.Status.TeamID == 0 {
|
||||
if err := r.createOrAdoptTeam(ctx, &team, instanceClient); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
if team.Status.CredentialsSecretRef == nil {
|
||||
if err := r.mintTeamCredential(ctx, &team, instanceClient); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
teamKey, err := readOperatorSecret(ctx, r.Client, r.OperatorNamespace, team.Status.CredentialsSecretRef)
|
||||
// Idempotent on the external id: the same team comes back whether this is
|
||||
// the first reconcile, a retry after a lost status write, or a repair after
|
||||
// somebody deleted the team behind our back.
|
||||
created, err := tc.CreateTeam(ctx, team.Spec.DisplayName, teamExternalID(&team))
|
||||
if err != nil {
|
||||
return ctrl.Result{}, err
|
||||
if se, ok := errors.AsType[*tdclient.StatusError](err); ok && se.Code == http.StatusConflict {
|
||||
return r.setTeamNotReady(ctx, &team, terdutv1alpha1.ReasonTeamNameTaken, nameTakenMessage(&team))
|
||||
}
|
||||
return ctrl.Result{}, fmt.Errorf("POST /api/teams: %w", err)
|
||||
}
|
||||
teamClient := newClient(endpoint).WithToken(teamKey)
|
||||
team.Status.TeamID = created.ID
|
||||
|
||||
// Owner-gated on terdut-server, so this always runs with the
|
||||
// team-scoped credential just minted above, never the instance-scoped
|
||||
// one used to create the team (internal/api/teams.go's requireTeamOwner
|
||||
// has no branch for an instance-scoped service account, confirmed
|
||||
// against source). Applied unconditionally rather than diffed against a
|
||||
// stored "last-applied" value: both calls are idempotent PUTs of the
|
||||
// whole resource, the same "cheap because it's small" reasoning §5
|
||||
// already applies to the escalation policy's whole-policy PUT.
|
||||
if err := teamClient.RenameTeam(ctx, team.Status.TeamID, team.Spec.DisplayName); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("PUT /api/teams/%d: %w", team.Status.TeamID, err)
|
||||
if err := tc.RenameTeam(ctx, created.ID, team.Spec.DisplayName); err != nil {
|
||||
if se, ok := errors.AsType[*tdclient.StatusError](err); ok && se.Code == http.StatusConflict {
|
||||
return r.setTeamNotReady(ctx, &team, terdutv1alpha1.ReasonTeamNameTaken, nameTakenMessage(&team))
|
||||
}
|
||||
return ctrl.Result{}, fmt.Errorf("PUT /api/teams/%d: %w", created.ID, err)
|
||||
}
|
||||
if err := teamClient.SetTeamOIDCGroups(ctx, team.Status.TeamID, team.Spec.OIDC.MemberGroup, team.Spec.OIDC.OwnerGroup); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("PUT /api/teams/%d/oidc-groups: %w", team.Status.TeamID, err)
|
||||
if err := tc.SetTeamOIDCGroups(ctx, created.ID, team.Spec.OIDC.MemberGroup, team.Spec.OIDC.OwnerGroup); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("PUT /api/teams/%d/oidc-groups: %w", created.ID, err)
|
||||
}
|
||||
if err := r.reconcileInvite(ctx, &team, teamClient); err != nil {
|
||||
|
||||
if condErr, err := r.reconcileEscalation(ctx, tc, &team); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
} else if condErr != nil {
|
||||
return r.setTeamNotReady(ctx, &team, condErr.reason, condErr.message)
|
||||
}
|
||||
if condErr, err := r.reconcileDeadmanSwitches(ctx, tc, &team); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
} else if condErr != nil {
|
||||
return r.setTeamNotReady(ctx, &team, condErr.reason, condErr.message)
|
||||
}
|
||||
|
||||
meta.SetStatusCondition(&team.Status.Conditions, metav1.Condition{
|
||||
Type: terdutv1alpha1.ConditionReady,
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: terdutv1alpha1.ReasonTeamAdopted,
|
||||
Message: fmt.Sprintf("team %d ready, credentials in Secret %q", team.Status.TeamID, team.Status.CredentialsSecretRef.Name),
|
||||
Message: fmt.Sprintf("team %d applied", team.Status.TeamID),
|
||||
})
|
||||
team.Status.ObservedGeneration = team.Generation
|
||||
if err := r.Status().Update(ctx, &team); err != nil {
|
||||
@@ -147,13 +133,31 @@ func (r *TerdutTeamReconciler) Reconcile(ctx context.Context, req ctrl.Request)
|
||||
}
|
||||
if r.Recorder != nil {
|
||||
r.Recorder.Eventf(&team, nil, corev1.EventTypeNormal, terdutv1alpha1.ReasonTeamAdopted, terdutv1alpha1.ReasonTeamAdopted,
|
||||
"team ready")
|
||||
"team applied")
|
||||
}
|
||||
log.Info("TerdutTeam ready", "name", team.Name, "teamID", team.Status.TeamID)
|
||||
|
||||
return ctrl.Result{RequeueAfter: resyncInterval}, nil
|
||||
}
|
||||
|
||||
func nameTakenMessage(team *terdutv1alpha1.TerdutTeam) string {
|
||||
return fmt.Sprintf("the server already has a different team named %q; choose another spec.displayName, "+
|
||||
"or remove that team", team.Spec.DisplayName)
|
||||
}
|
||||
|
||||
// serverClient builds a client for srv, authenticated with its operator key.
|
||||
func (r *TerdutTeamReconciler) serverClient(ctx context.Context, srv *terdutv1alpha1.TerdutServer) (*tdclient.Client, error) {
|
||||
key, err := readOperatorKey(ctx, r.Client, srv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newClient := r.NewClient
|
||||
if newClient == nil {
|
||||
newClient = tdclient.New
|
||||
}
|
||||
return newClient(serviceURL(srv)).WithToken(key), nil
|
||||
}
|
||||
|
||||
// teamError carries a condition reason/message, the same role databaseError
|
||||
// plays for TerdutServer: an expected, requeue-and-retry outcome, not a
|
||||
// reconcile failure.
|
||||
@@ -168,6 +172,21 @@ func (e *teamError) Error() string { return e.message }
|
||||
// consent check (DESIGN.md §4.6) when serverRef.namespace differs from this
|
||||
// TerdutTeam's own.
|
||||
func (r *TerdutTeamReconciler) resolveServerRef(ctx context.Context, team *terdutv1alpha1.TerdutTeam) (*terdutv1alpha1.TerdutServer, *teamError) {
|
||||
srv, ns, getErr := r.getServerRef(ctx, team)
|
||||
if getErr != nil {
|
||||
return nil, getErr
|
||||
}
|
||||
if ns == team.Namespace {
|
||||
return srv, nil
|
||||
}
|
||||
return r.checkConsent(ctx, team, srv, ns)
|
||||
}
|
||||
|
||||
// getServerRef fetches the referenced TerdutServer and the namespace it was
|
||||
// looked up in, without the consent check. Deleting a team uses it directly:
|
||||
// consent gates what the operator will start acting on, not whether it may
|
||||
// clean up after itself once the server owner has narrowed allowedTeams.
|
||||
func (r *TerdutTeamReconciler) getServerRef(ctx context.Context, team *terdutv1alpha1.TerdutTeam) (*terdutv1alpha1.TerdutServer, string, *teamError) {
|
||||
ns := team.Spec.ServerRef.Namespace
|
||||
if ns == "" {
|
||||
ns = team.Namespace
|
||||
@@ -176,18 +195,19 @@ func (r *TerdutTeamReconciler) resolveServerRef(ctx context.Context, team *terdu
|
||||
var srv terdutv1alpha1.TerdutServer
|
||||
if err := r.Get(ctx, client.ObjectKey{Namespace: ns, Name: team.Spec.ServerRef.Name}, &srv); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return nil, &teamError{
|
||||
return nil, ns, &teamError{
|
||||
reason: terdutv1alpha1.ReasonServerRefNotFound,
|
||||
message: fmt.Sprintf("TerdutServer %q not found in namespace %q", team.Spec.ServerRef.Name, ns),
|
||||
}
|
||||
}
|
||||
return nil, &teamError{reason: terdutv1alpha1.ReasonServerRefNotFound, message: err.Error()}
|
||||
}
|
||||
|
||||
if ns == team.Namespace {
|
||||
return &srv, nil
|
||||
return nil, ns, &teamError{reason: terdutv1alpha1.ReasonServerRefNotFound, message: err.Error()}
|
||||
}
|
||||
return &srv, ns, nil
|
||||
}
|
||||
|
||||
// checkConsent applies DESIGN.md §4.6's allowedTeams gate to a cross-namespace
|
||||
// reference.
|
||||
func (r *TerdutTeamReconciler) checkConsent(ctx context.Context, team *terdutv1alpha1.TerdutTeam, srv *terdutv1alpha1.TerdutServer, ns string) (*terdutv1alpha1.TerdutServer, *teamError) {
|
||||
var ownNamespace corev1.Namespace
|
||||
if err := r.Get(ctx, client.ObjectKey{Name: team.Namespace}, &ownNamespace); err != nil {
|
||||
return nil, &teamError{
|
||||
@@ -206,11 +226,11 @@ func (r *TerdutTeamReconciler) resolveServerRef(ctx context.Context, team *terdu
|
||||
ns, team.Spec.ServerRef.Name, team.Namespace),
|
||||
}
|
||||
}
|
||||
return &srv, nil
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
func (r *TerdutTeamReconciler) setTeamNotReady(
|
||||
ctx context.Context, team *terdutv1alpha1.TerdutTeam, reason, message string, d time.Duration,
|
||||
ctx context.Context, team *terdutv1alpha1.TerdutTeam, reason, message string,
|
||||
) (ctrl.Result, error) {
|
||||
meta.SetStatusCondition(&team.Status.Conditions, metav1.Condition{
|
||||
Type: terdutv1alpha1.ConditionReady,
|
||||
@@ -225,42 +245,18 @@ func (r *TerdutTeamReconciler) setTeamNotReady(
|
||||
if r.Recorder != nil {
|
||||
r.Recorder.Eventf(team, nil, corev1.EventTypeWarning, reason, reason, message)
|
||||
}
|
||||
return ctrl.Result{RequeueAfter: d}, nil
|
||||
return ctrl.Result{RequeueAfter: waitInterval}, nil
|
||||
}
|
||||
|
||||
// teamCredentialsSecretName/teamServiceAccountName follow the same
|
||||
// <namespace>.<name>-suffix convention TerdutServer's secrets use
|
||||
// (DESIGN.md §6 point 3), keyed on the TerdutTeam CR's own identity rather
|
||||
// than its (mutable) displayName -- a service account's own name is
|
||||
// globally unique across the whole install (internal/db/migrations/
|
||||
// 014_service_accounts.sql's UNIQUE constraint, confirmed against source),
|
||||
// so this has to be collision-safe the same way the credentials Secret
|
||||
// names already are.
|
||||
func teamCredentialsSecretName(team *terdutv1alpha1.TerdutTeam) string {
|
||||
return fmt.Sprintf("%s.%s-team-credentials", team.Namespace, team.Name)
|
||||
}
|
||||
|
||||
func teamServiceAccountName(team *terdutv1alpha1.TerdutTeam) string {
|
||||
return fmt.Sprintf("terdut-team.%s.%s", team.Namespace, team.Name)
|
||||
}
|
||||
|
||||
// reconcileTeamDelete cleans up the team-scoped credentials Secret and, if
|
||||
// one was ever minted, deletes the team server-side first. If
|
||||
// status.teamID or status.credentialsSecretRef was never set (the CR was
|
||||
// deleted before reconciliation ever got that far), there is deliberately
|
||||
// no attempt to clean up server-side: terdut-server's DELETE /api/teams/
|
||||
// {teamID} is owner-gated (requireTeamOwner), and an instance-scoped
|
||||
// credential -- the only one this controller would otherwise hold -- does
|
||||
// not satisfy that check (confirmed against source, same finding as
|
||||
// TEAM-LOOKUP.md's). A team created but never fully reconciled to Ready is
|
||||
// left orphaned server-side for a human with real owner/admin access to
|
||||
// clean up -- a known, documented limitation, not a silent gap.
|
||||
// reconcileTeamDelete removes the team on the server, then the finalizer. The
|
||||
// server refuses while the team has open incidents (409), which surfaces as a
|
||||
// retried error: tidying up must not be how a live page disappears.
|
||||
func (r *TerdutTeamReconciler) reconcileTeamDelete(ctx context.Context, team *terdutv1alpha1.TerdutTeam) (ctrl.Result, error) {
|
||||
if !controllerutil.ContainsFinalizer(team, teamFinalizerName) {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
if team.Status.TeamID != 0 && team.Status.CredentialsSecretRef != nil {
|
||||
if team.Status.TeamID != 0 {
|
||||
if err := r.deleteTeamServerSide(ctx, team); err != nil {
|
||||
if r.Recorder != nil {
|
||||
r.Recorder.Eventf(team, nil, corev1.EventTypeWarning, "DeleteFailed", "DeleteFailed", err.Error())
|
||||
@@ -269,40 +265,33 @@ func (r *TerdutTeamReconciler) reconcileTeamDelete(ctx context.Context, team *te
|
||||
}
|
||||
}
|
||||
|
||||
secret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: teamCredentialsSecretName(team), Namespace: r.OperatorNamespace}}
|
||||
if err := r.Delete(ctx, secret); err != nil && !apierrors.IsNotFound(err) {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
controllerutil.RemoveFinalizer(team, teamFinalizerName)
|
||||
return ctrl.Result{}, r.Update(ctx, team)
|
||||
}
|
||||
|
||||
func (r *TerdutTeamReconciler) deleteTeamServerSide(ctx context.Context, team *terdutv1alpha1.TerdutTeam) error {
|
||||
srv, resolveErr := r.resolveServerRef(ctx, team)
|
||||
if resolveErr != nil {
|
||||
// The TerdutServer (or the namespace consent for it) is gone too --
|
||||
// most likely the whole install is being torn down together.
|
||||
// Nothing to delete against; proceed rather than block forever on
|
||||
// a parent that no longer exists.
|
||||
return nil
|
||||
}
|
||||
teamKey, err := readOperatorSecret(ctx, r.Client, r.OperatorNamespace, team.Status.CredentialsSecretRef)
|
||||
if err != nil {
|
||||
// Consent is deliberately not checked here: it gates what the operator
|
||||
// starts acting on, not whether it may clean up after itself once the
|
||||
// server owner has narrowed allowedTeams. Only a TerdutServer that is
|
||||
// really gone lets the delete proceed (there is nothing to delete against);
|
||||
// any other failure is retried rather than orphaning the team.
|
||||
srv, ns, getErr := r.getServerRef(ctx, team)
|
||||
if getErr != nil {
|
||||
var probe terdutv1alpha1.TerdutServer
|
||||
err := r.Get(ctx, client.ObjectKey{Namespace: ns, Name: team.Spec.ServerRef.Name}, &probe)
|
||||
if apierrors.IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("reading TerdutServer %q/%q: %w", ns, team.Spec.ServerRef.Name, getErr)
|
||||
}
|
||||
tc, err := r.serverClient(ctx, srv)
|
||||
if err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return nil // the key Secret went with its TerdutServer
|
||||
}
|
||||
return err
|
||||
}
|
||||
newClient := r.NewClient
|
||||
if newClient == nil {
|
||||
newClient = tdclient.New
|
||||
}
|
||||
err = newClient(serviceURL(srv)).WithToken(teamKey).DeleteTeam(ctx, team.Status.TeamID)
|
||||
if statusErr, ok := errors.AsType[*tdclient.StatusError](err); ok && statusErr.Code == http.StatusNotFound {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
return tc.DeleteTeam(ctx, team.Status.TeamID)
|
||||
}
|
||||
|
||||
// SetupWithManager sets up the controller with the Manager.
|
||||
@@ -315,6 +304,28 @@ func (r *TerdutTeamReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
}
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&terdutv1alpha1.TerdutTeam{}).
|
||||
Watches(&terdutv1alpha1.TerdutServer{}, handler.EnqueueRequestsFromMapFunc(r.teamsForServer)).
|
||||
Named("terdutteam").
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
// teamsForServer re-reconciles every TerdutTeam that references a TerdutServer
|
||||
// when it changes (becomes Ready, say), instead of polling for it.
|
||||
func (r *TerdutTeamReconciler) teamsForServer(ctx context.Context, obj client.Object) []reconcile.Request {
|
||||
var teams terdutv1alpha1.TerdutTeamList
|
||||
if err := r.List(ctx, &teams); err != nil {
|
||||
return nil
|
||||
}
|
||||
var out []reconcile.Request
|
||||
for i := range teams.Items {
|
||||
t := &teams.Items[i]
|
||||
ns := t.Spec.ServerRef.Namespace
|
||||
if ns == "" {
|
||||
ns = t.Namespace
|
||||
}
|
||||
if t.Spec.ServerRef.Name == obj.GetName() && ns == obj.GetNamespace() {
|
||||
out = append(out, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(t)})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user