fb9e6a38dc
CI / test (push) Has been cancelled
Both child CRDs resolve their own teamRef -> TerdutTeam.status via the new
shared resolveTeamAndClient helper (childref.go), never chaining up to
TerdutServer (DESIGN.md §5) -- TerdutTeam.status.serverEndpoint, added in
this same stage, is what makes that literally true.
TerdutEscalationRule: one PUT /api/teams/{id}/escalation per reconcile
(an upsert server-side, confirmed against source), resolving each "user"
target's username to a user_id via GET /api/users first and reporting
Ready: False, reason: UnknownUser if it doesn't resolve. No DELETE exists
for this resource, so its delete path PUTs an empty policy as the closest
available undo.
TerdutDeadmanSwitch: real create/update-in-place/delete, using
terdut-server v0.33.0's PUT (added specifically for this operator). No
unique-name constraint server-side, so idempotent-create here is
GET-list-and-match-by-name rather than adopt-on-409.
Extends tdclient with User/GetUserByUsername, the escalation request types
+ SetEscalation, and DeadmanSwitch + its CRUD methods. Also folds
ConditionTeamReady into the single shared ConditionReady constant, since
both were literally "Ready" and Stage 3 would otherwise have needed a
third same-valued constant.
internal/controller/terdutserver_controller_test.go's fakeTerdutServer
grows GET /api/users, PUT .../escalation, and the full dead man's switch
collection/item routes, replacing the old parseTeamPath/handleTeamByID
pair with a more general parseTeamSubPath/handleTeamSubPath dispatcher
that still covers every existing Stage 1/2 route unchanged.
make fmt lint test build all clean; envtest coverage for
internal/controller: 50.5% -> 71.7%.
55 lines
2.0 KiB
Go
55 lines
2.0 KiB
Go
package controller
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
|
|
terdutv1alpha1 "git.ryuvia.com/niklas/terdut-operator/api/v1alpha1"
|
|
"git.ryuvia.com/niklas/terdut-operator/internal/tdclient"
|
|
)
|
|
|
|
// childError carries a condition reason/message, the same role teamError
|
|
// and databaseError play for their own controllers: an expected,
|
|
// requeue-and-retry outcome, not a reconcile failure.
|
|
type childError struct {
|
|
reason string
|
|
message string
|
|
}
|
|
|
|
func (e *childError) Error() string { return e.message }
|
|
|
|
// resolveTeamAndClient implements DESIGN.md §5's "every child resolves its
|
|
// own teamRef -> TerdutTeam.status, never chains up to TerdutServer"
|
|
// rule -- shared by TerdutEscalationRule and TerdutDeadmanSwitch, which
|
|
// both need exactly this and nothing else to call terdut-server's API.
|
|
func resolveTeamAndClient(
|
|
ctx context.Context, c client.Client, operatorNamespace, namespace string,
|
|
ref terdutv1alpha1.TerdutTeamRef, newClient func(string) *tdclient.Client,
|
|
) (*terdutv1alpha1.TerdutTeam, *tdclient.Client, *childError) {
|
|
var team terdutv1alpha1.TerdutTeam
|
|
if err := c.Get(ctx, client.ObjectKey{Namespace: namespace, Name: ref.Name}, &team); err != nil {
|
|
if apierrors.IsNotFound(err) {
|
|
return nil, nil, &childError{
|
|
reason: terdutv1alpha1.ReasonTeamRefNotFound,
|
|
message: fmt.Sprintf("TerdutTeam %q not found in namespace %q", ref.Name, namespace),
|
|
}
|
|
}
|
|
return nil, nil, &childError{reason: terdutv1alpha1.ReasonTeamRefNotFound, message: err.Error()}
|
|
}
|
|
if team.Status.CredentialsSecretRef == nil || team.Status.TeamID == 0 {
|
|
return nil, nil, &childError{
|
|
reason: terdutv1alpha1.ReasonWaitingForTeam,
|
|
message: fmt.Sprintf("TerdutTeam %q is not Ready yet", ref.Name),
|
|
}
|
|
}
|
|
|
|
teamKey, err := readOperatorSecret(ctx, c, operatorNamespace, team.Status.CredentialsSecretRef)
|
|
if err != nil {
|
|
return nil, nil, &childError{reason: terdutv1alpha1.ReasonWaitingForTeam, message: err.Error()}
|
|
}
|
|
return &team, newClient(team.Status.ServerEndpoint).WithToken(teamKey), nil
|
|
}
|