// 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) } }