feat: add empty client spool reconciliation
This commit is contained in:
@@ -59,3 +59,34 @@ func Handshake(ctx context.Context, transport Transport, hello *rvboxv1.ClientHe
|
|||||||
}
|
}
|
||||||
return Session{ID: welcome.GetSessionId(), Generation: welcome.GetSessionGeneration(), Protocol: proto.Clone(payload.GetSelectedProtocol()).(*rvboxv1.ProtocolVersion)}, nil
|
return Session{ID: welcome.GetSessionId(), Generation: welcome.GetSessionGeneration(), Protocol: proto.Clone(payload.GetSelectedProtocol()).(*rvboxv1.ProtocolVersion)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reconcile sends the complete retained client state after welcome. The caller
|
||||||
|
// must apply the returned result durably before accepting fresh dispatch.
|
||||||
|
func Reconcile(ctx context.Context, transport Transport, session Session, snapshot *rvboxv1.ReconcileSnapshot, limits agentproto.Limits) (*rvboxv1.ReconcileResult, error) {
|
||||||
|
if transport == nil || session.ID == "" || session.Generation == 0 || snapshot == nil {
|
||||||
|
return nil, ErrProtocolHandshake
|
||||||
|
}
|
||||||
|
request := &rvboxv1.AgentEnvelope{SessionId: session.ID, SessionGeneration: session.Generation, Payload: &rvboxv1.AgentEnvelope_ReconcileSnapshot{ReconcileSnapshot: snapshot}}
|
||||||
|
if err := agentproto.ValidateEnvelope(request, limits, rvboxv1.Platform_PLATFORM_WINDOWS); err != nil {
|
||||||
|
return nil, fmt.Errorf("validate reconciliation snapshot: %w", err)
|
||||||
|
}
|
||||||
|
encoded, err := proto.Marshal(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := transport.Write(ctx, encoded); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
received, err := transport.Read(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result, err := agentproto.DecodeEnvelope(received, limits, rvboxv1.Platform_PLATFORM_WINDOWS)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decode ReconcileResult: %w", err)
|
||||||
|
}
|
||||||
|
if result.GetSessionId() != session.ID || result.GetSessionGeneration() != session.Generation || result.GetReconcileResult() == nil {
|
||||||
|
return nil, ErrProtocolHandshake
|
||||||
|
}
|
||||||
|
return proto.Clone(result.GetReconcileResult()).(*rvboxv1.ReconcileResult), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -46,6 +46,24 @@ func TestHandshakeRejectsNonWelcome_BH_SES_07(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReconcileSnapshot_HP_SES_08(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
result := &rvboxv1.AgentEnvelope{SessionId: "issued-session", SessionGeneration: 7, Payload: &rvboxv1.AgentEnvelope_ReconcileResult{ReconcileResult: &rvboxv1.ReconcileResult{}}}
|
||||||
|
encoded, err := proto.Marshal(result)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
transport := &fakeTransport{read: encoded}
|
||||||
|
reconciled, err := Reconcile(context.Background(), transport, Session{ID: "issued-session", Generation: 7}, &rvboxv1.ReconcileSnapshot{}, agentproto.DefaultLimits())
|
||||||
|
if err != nil || reconciled == nil {
|
||||||
|
t.Fatalf("Reconcile = %#v, %v", reconciled, err)
|
||||||
|
}
|
||||||
|
var sent rvboxv1.AgentEnvelope
|
||||||
|
if err := proto.Unmarshal(transport.written, &sent); err != nil || sent.GetReconcileSnapshot() == nil || sent.GetSessionId() != "issued-session" || sent.GetSessionGeneration() != 7 {
|
||||||
|
t.Fatalf("sent snapshot valid=%t session=%q generation=%d err=%v", sent.GetReconcileSnapshot() != nil, sent.GetSessionId(), sent.GetSessionGeneration(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type fakeTransport struct {
|
type fakeTransport struct {
|
||||||
written []byte
|
written []byte
|
||||||
read []byte
|
read []byte
|
||||||
|
|||||||
@@ -184,6 +184,22 @@ func (server *AgentServer) serveConnection(parent context.Context, connection *w
|
|||||||
server.close(connection, websocket.StatusPolicyViolation, ErrStaleSession.Error())
|
server.close(connection, websocket.StatusPolicyViolation, ErrStaleSession.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if snapshot := envelope.GetReconcileSnapshot(); snapshot != nil {
|
||||||
|
// A retained command requires the command-store reconciliation phase,
|
||||||
|
// which is deliberately not substituted with a no-op result.
|
||||||
|
if len(snapshot.GetRetainedCommands()) != 0 {
|
||||||
|
server.close(connection, websocket.StatusPolicyViolation, "reconciliation for retained commands is unavailable")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
encoded, err := proto.Marshal(&rvboxv1.AgentEnvelope{
|
||||||
|
SessionId: encodedSessionID, SessionGeneration: registration.Generation,
|
||||||
|
Payload: &rvboxv1.AgentEnvelope_ReconcileResult{ReconcileResult: &rvboxv1.ReconcileResult{}},
|
||||||
|
})
|
||||||
|
if err != nil || queue.EnqueueControl(Frame{Kind: FrameControl, Payload: encoded}) != nil {
|
||||||
|
server.close(connection, websocket.StatusInternalError, "could not queue reconciliation result")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -170,6 +170,12 @@ layer = "unit"
|
|||||||
status = "implemented"
|
status = "implemented"
|
||||||
tests = ["internal/client/agent/handshake_test.go:TestHandshakeRejectsNonWelcome_BH_SES_07"]
|
tests = ["internal/client/agent/handshake_test.go:TestHandshakeRejectsNonWelcome_BH_SES_07"]
|
||||||
|
|
||||||
|
[[requirements]]
|
||||||
|
id = "HP-SES-08"
|
||||||
|
layer = "unit"
|
||||||
|
status = "implemented"
|
||||||
|
tests = ["internal/client/agent/handshake_test.go:TestReconcileSnapshot_HP_SES_08"]
|
||||||
|
|
||||||
[[requirements]]
|
[[requirements]]
|
||||||
id = "HP-SES-05"
|
id = "HP-SES-05"
|
||||||
layer = "integration"
|
layer = "integration"
|
||||||
|
|||||||
@@ -39,4 +39,7 @@ func TestWebSocketHelloWelcome_HP_SES_06(t *testing.T) {
|
|||||||
if result.ID == "" || result.Generation != 1 || result.Protocol.GetMajor() != 1 {
|
if result.ID == "" || result.Generation != 1 || result.Protocol.GetMajor() != 1 {
|
||||||
t.Fatalf("handshake result = %#v", result)
|
t.Fatalf("handshake result = %#v", result)
|
||||||
}
|
}
|
||||||
|
if reconciled, err := agent.Reconcile(ctx, transport, result, &rvboxv1.ReconcileSnapshot{}, agentproto.DefaultLimits()); err != nil || reconciled == nil {
|
||||||
|
t.Fatalf("empty reconciliation = %#v, %v", reconciled, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user