- PATCH /agents/{id} switches the mode and reconnects the agent
- POST /revisions/{n}/deploy creates pending push deployments, sends
ApplyRevisionCommand with the deployment ID, re-sends open pushes on
connect and announces them in GetDesiredState; reports with the ID
complete the deployment
- POST /revisions/{n}/rollback re-renders the stored generator input
with the next revision number
- GET /revisions/{n}/diff and GET /config/pending with unified diffs
- GET /events streams agent, deployment, revision and pending events
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
130 lines
5.1 KiB
Go
130 lines
5.1 KiB
Go
// SPDX-License-Identifier: LicenseRef-DDI-Proprietary
|
|
|
|
package agentapi
|
|
|
|
import (
|
|
"net/netip"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
|
|
agentv1 "git.beugsiud.de/beug.k/DDI/proto/gen/ddi/agent/v1"
|
|
"git.beugsiud.de/beug.k/DDI/server/internal/ipam"
|
|
"git.beugsiud.de/beug.k/DDI/server/internal/service"
|
|
)
|
|
|
|
// connect opens a stream, sends Hello and returns the Welcome.
|
|
func connect(t *testing.T, a *testAgent, applied uint64) (agentv1.AgentService_ConnectClient, *agentv1.Welcome) {
|
|
t.Helper()
|
|
stream, err := a.client.Connect(ctxT(t))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := stream.Send(&agentv1.ConnectRequest{Message: &agentv1.ConnectRequest_Hello{Hello: &agentv1.Hello{
|
|
AppliedRevision: applied, LastGoodRevision: applied}}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
msg, err := stream.Recv()
|
|
if err != nil || msg.GetWelcome() == nil {
|
|
t.Fatalf("welcome %v %v", msg, err)
|
|
}
|
|
return stream, msg.GetWelcome()
|
|
}
|
|
|
|
func TestPushModesAndReports(t *testing.T) {
|
|
e := newTestEnv(t)
|
|
ctx := ctxT(t)
|
|
a := e.enroll("dns")
|
|
e.configureGenerator(3600)
|
|
if _, _, err := e.svc.CreateRevision(ctx, service.SystemActor, ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if ag, err := e.svc.SetAgentMode(ctx, service.SystemActor, a.id, "manual"); err != nil || ag.TargetRevision != 1 {
|
|
t.Fatalf("mode %+v %v", ag, err)
|
|
}
|
|
|
|
// A push while the agent is offline is announced by polling ...
|
|
targets, err := e.svc.DeployRevision(ctx, service.SystemActor, 1, []string{a.id})
|
|
if err != nil || len(targets) != 1 || targets[0].Sent {
|
|
t.Fatalf("deploy %+v %v", targets, err)
|
|
}
|
|
id := strconv.FormatInt(targets[0].DeploymentID, 10)
|
|
st, err := a.client.GetDesiredState(ctx, &agentv1.GetDesiredStateRequest{})
|
|
if err != nil || st.GetDeploymentId() != id || st.GetDesiredRevision() != 1 || st.GetMode() != agentv1.OperatingMode_OPERATING_MODE_MANUAL {
|
|
t.Fatalf("desired state %v %v", st, err)
|
|
}
|
|
// ... and sent again when it connects.
|
|
stream, w := connect(t, a, 0)
|
|
if w.GetMode() != agentv1.OperatingMode_OPERATING_MODE_MANUAL || w.GetDesiredRevision() != 1 {
|
|
t.Errorf("welcome %v", w)
|
|
}
|
|
msg, err := stream.Recv()
|
|
if err != nil || msg.GetCommand().GetApplyRevision().GetRevision() != 1 || msg.GetCommand().GetApplyRevision().GetDeploymentId() != id {
|
|
t.Fatalf("pending push %v %v", msg, err)
|
|
}
|
|
|
|
// The report completes the deployment; it is not announced any more and
|
|
// a resent report is accepted without changes.
|
|
report := &agentv1.ReportDeploymentRequest{DeploymentId: id, Revision: 1,
|
|
Trigger: agentv1.DeploymentTrigger_DEPLOYMENT_TRIGGER_PUSH, Outcome: agentv1.DeploymentOutcome_DEPLOYMENT_OUTCOME_SUCCEEDED,
|
|
ActiveRevision: 1, StartedAt: timestamppb.New(time.Now().Add(-time.Second)), FinishedAt: timestamppb.Now()}
|
|
for range 2 {
|
|
if _, err := a.client.ReportDeployment(ctx, report); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
deps, err := e.svc.ListDeployments(ctx, service.DeploymentFilter{AgentID: a.id})
|
|
if err != nil || len(deps.Items) != 1 || deps.Items[0].Outcome != "succeeded" || deps.Items[0].FinishedAt == nil ||
|
|
deps.Items[0].RequestedByName != "system" {
|
|
t.Fatalf("deployments %+v %v", deps, err)
|
|
}
|
|
if st, err := a.client.GetDesiredState(ctx, &agentv1.GetDesiredStateRequest{}); err != nil || st.GetDeploymentId() != "" {
|
|
t.Errorf("desired state after report %v %v", st, err)
|
|
}
|
|
|
|
// A manual agent gets no new revisions, but a push while connected.
|
|
var netID string
|
|
if err := e.st.Pool().QueryRow(ctx, `SELECT id FROM networks`).Scan(&netID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := e.svc.CreateAddress(ctx, service.SystemActor, service.AddressInput{NetworkID: netID,
|
|
IP: netip.MustParseAddr("192.0.2.10"), Type: ipam.TypeStatic, Hostname: "www"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rev, created, err := e.svc.CreateRevision(ctx, service.SystemActor, ""); err != nil || !created || rev.Number != 2 {
|
|
t.Fatalf("revision 2 %+v %v %v", rev, created, err)
|
|
}
|
|
targets, err = e.svc.DeployRevision(ctx, service.SystemActor, 2, nil)
|
|
if err != nil || len(targets) != 1 || !targets[0].Sent {
|
|
t.Fatalf("push %+v %v", targets, err)
|
|
}
|
|
msg, err = stream.Recv()
|
|
if err != nil || msg.GetCommand().GetApplyRevision().GetRevision() != 2 ||
|
|
msg.GetCommand().GetApplyRevision().GetDeploymentId() != strconv.FormatInt(targets[0].DeploymentID, 10) {
|
|
t.Fatalf("push command %v %v", msg, err)
|
|
}
|
|
|
|
// A mode change reconnects the agent with the new mode.
|
|
if _, err := e.svc.SetAgentMode(ctx, service.SystemActor, a.id, "auto_pull"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err = stream.Recv()
|
|
wantCode(t, err, codes.Unavailable)
|
|
stream, w = connect(t, a, 1)
|
|
if w.GetMode() != agentv1.OperatingMode_OPERATING_MODE_AUTO_PULL || w.GetDesiredRevision() != 2 {
|
|
t.Errorf("welcome after mode change %v", w)
|
|
}
|
|
// The open push of revision 2 is still pending (the desired revision).
|
|
msg, err = stream.Recv()
|
|
if err != nil || msg.GetCommand().GetApplyRevision().GetRevision() != 2 {
|
|
t.Errorf("open push after reconnect %v %v", msg, err)
|
|
}
|
|
eventually(t, "connected", func() bool { return e.hub.Connected(a.id) })
|
|
if err := stream.CloseSend(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|