From 8afb2e09639620946a8b3b46cebce458dcb43053 Mon Sep 17 00:00:00 2001 From: MadhaviLosetty Date: Tue, 2 Jun 2026 19:06:57 -0700 Subject: [PATCH] feat(rps): display structured per-component activation result Parse and surface the structured per-component provisioning result that RPS now sends (rps#2665). When the success message includes the additive Components object, log each component (Activation, Wired/ Wireless Network, TLS, CIRA Proxy/Connection) with its result and details, logging failures at error level. When Components is absent (older RPS), fall back to the existing flat status lines, so behavior against older servers is unchanged. Refs device-management-toolkit/rps#2665 --- internal/rps/message.go | 22 ++++++++++++++++++++ internal/rps/rps.go | 45 ++++++++++++++++++++++++++++++++++++---- internal/rps/rps_test.go | 37 +++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 4 deletions(-) diff --git a/internal/rps/message.go b/internal/rps/message.go index b7093915..a57bdf33 100644 --- a/internal/rps/message.go +++ b/internal/rps/message.go @@ -40,6 +40,28 @@ type StatusMessage struct { Network string `json:"Network,omitempty"` CIRAConnection string `json:"CIRAConnection,omitempty"` TLSConfiguration string `json:"TLSConfiguration,omitempty"` + // Components carries the additive, structured per-component provisioning result + // introduced by RPS (rps#2665). Older RPS releases omit it; in that case the flat + // fields above remain the source of truth. + Components *ComponentResults `json:"Components,omitempty"` +} + +// ComponentResult is the per-component outcome of an activation step. +type ComponentResult struct { + Result string `json:"Result"` + Mode string `json:"Mode,omitempty"` + Details string `json:"Details,omitempty"` + ErrorCode int `json:"ErrorCode"` +} + +// ComponentResults groups the structured per-component activation results. +type ComponentResults struct { + Activation *ComponentResult `json:"Activation,omitempty"` + WiredNetwork *ComponentResult `json:"WiredNetwork,omitempty"` + WirelessNetwork *ComponentResult `json:"WirelessNetwork,omitempty"` + TLS *ComponentResult `json:"TLS,omitempty"` + CIRAProxy *ComponentResult `json:"CIRAProxy,omitempty"` + CIRAConnection *ComponentResult `json:"CIRAConnection,omitempty"` } // MessagePayload struct is used for the initial request to RPS to activate or manage a device diff --git a/internal/rps/rps.go b/internal/rps/rps.go index 37a3ae5c..8f3ecf5e 100644 --- a/internal/rps/rps.go +++ b/internal/rps/rps.go @@ -235,10 +235,7 @@ func (amt *AMTActivationServer) ProcessMessage(message []byte) []byte { log.Error(err) log.Info(activation.Message) } else { - log.Info("Status: " + statusMessage.Status) - log.Info("Network: " + statusMessage.Network) - log.Info("CIRA: " + statusMessage.CIRAConnection) - log.Info("TLS: " + statusMessage.TLSConfiguration) + logStatusMessage(statusMessage) } return nil @@ -263,6 +260,46 @@ func (amt *AMTActivationServer) ProcessMessage(message []byte) []byte { return msgPayload } +// logStatusMessage reports the activation result. When RPS provides the structured +// per-component breakdown (rps#2665) it is logged component-by-component; otherwise the +// legacy flat status fields are logged for compatibility with older RPS releases. +func logStatusMessage(statusMessage StatusMessage) { + if statusMessage.Components != nil { + c := statusMessage.Components + logComponentResult("Activation", c.Activation) + logComponentResult("Wired Network", c.WiredNetwork) + logComponentResult("Wireless Network", c.WirelessNetwork) + logComponentResult("TLS", c.TLS) + logComponentResult("CIRA Proxy", c.CIRAProxy) + logComponentResult("CIRA Connection", c.CIRAConnection) + + return + } + + log.Info("Status: " + statusMessage.Status) + log.Info("Network: " + statusMessage.Network) + log.Info("CIRA: " + statusMessage.CIRAConnection) + log.Info("TLS: " + statusMessage.TLSConfiguration) +} + +// logComponentResult logs a single component result, skipping components RPS did not report. +func logComponentResult(label string, result *ComponentResult) { + if result == nil { + return + } + + line := label + ": " + result.Result + if result.Details != "" { + line += " (" + result.Details + ")" + } + + if result.Result == "Failure" { + log.Error(line) + } else { + log.Info(line) + } +} + func (amt *AMTActivationServer) GenerateHeartbeatResponse(activation Message) ([]byte, error) { activation.Method = "heartbeat_response" activation.Status = "success" diff --git a/internal/rps/rps_test.go b/internal/rps/rps_test.go index 0006e82e..0e1824d9 100644 --- a/internal/rps/rps_test.go +++ b/internal/rps/rps_test.go @@ -227,6 +227,43 @@ func TestProcessMessageSuccess(t *testing.T) { assert.Nil(t, decodedMessage) } +func TestProcessMessageStructuredSuccess(t *testing.T) { + // RPS rps#2665 structured per-component result rides alongside the legacy flat fields. + activation := `{ + "method": "success", + "message": "{\"Status\":\"Admin control mode.\",\"Components\":{\"Activation\":{\"Result\":\"Success\",\"Mode\":\"Admin control mode.\",\"ErrorCode\":0},\"WirelessNetwork\":{\"Result\":\"Failure\",\"Details\":\"Failed to add 1\",\"ErrorCode\":1}}}" + }` + server := NewAMTActivationServer(testFlags) + server.Connect(true) + decodedMessage := server.ProcessMessage([]byte(activation)) + assert.Nil(t, decodedMessage) +} + +func TestLogStatusMessageStructured(t *testing.T) { + // Structured Components present: per-component lines are logged, no panic. + statusMessage := StatusMessage{ + Status: "Admin control mode.", + Components: &ComponentResults{ + Activation: &ComponentResult{Result: "Success", Mode: "Admin control mode.", ErrorCode: 0}, + WirelessNetwork: &ComponentResult{Result: "Failure", Details: "Failed to add 1", ErrorCode: 1}, + }, + } + + assert.NotPanics(t, func() { logStatusMessage(statusMessage) }) +} + +func TestLogStatusMessageLegacyFallback(t *testing.T) { + // No Components: falls back to legacy flat-field logging. + statusMessage := StatusMessage{ + Status: "Admin control mode.", + Network: "configured", + CIRAConnection: "configured", + TLSConfiguration: "configured", + } + + assert.NotPanics(t, func() { logStatusMessage(statusMessage) }) +} + func TestProcessMessageUnformattedSuccess(t *testing.T) { activation := `{ "method": "success",