fix: sandbox binding + agent sandbox sync + uuid rotation
This commit is contained in:
parent
d1195bbc75
commit
17909d57d2
119
api/admin_assume.go
Normal file
119
api/admin_assume.go
Normal file
@ -0,0 +1,119 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"account/internal/store"
|
||||
)
|
||||
|
||||
const (
|
||||
assumeSandboxEmail = sandboxUserEmail
|
||||
)
|
||||
|
||||
func (h *handler) adminAssume(c *gin.Context) {
|
||||
adminUser, ok := h.requireAdminPermission(c, permissionAdminSettingsWrite)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !h.isRootAccount(adminUser) {
|
||||
respondError(c, http.StatusForbidden, "root_only", "root only")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
respondError(c, http.StatusBadRequest, "invalid_request", "invalid request payload")
|
||||
return
|
||||
}
|
||||
|
||||
email := strings.ToLower(strings.TrimSpace(req.Email))
|
||||
if email != assumeSandboxEmail {
|
||||
respondError(c, http.StatusBadRequest, "invalid_target", "target is not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
// Resolve sandbox user.
|
||||
sandboxUser, err := h.store.GetUserByEmail(c.Request.Context(), assumeSandboxEmail)
|
||||
if err != nil {
|
||||
if err == store.ErrUserNotFound {
|
||||
respondError(c, http.StatusNotFound, "sandbox_missing", "sandbox user not found")
|
||||
return
|
||||
}
|
||||
respondError(c, http.StatusInternalServerError, "sandbox_lookup_failed", "failed to lookup sandbox user")
|
||||
return
|
||||
}
|
||||
|
||||
// Rotate sandbox UUID if expired (hourly forced rotation).
|
||||
if err := h.ensureSandboxProxyUUID(c.Request.Context(), sandboxUser); err != nil {
|
||||
respondError(c, http.StatusInternalServerError, "sandbox_uuid_rotation_failed", "failed to rotate sandbox uuid")
|
||||
return
|
||||
}
|
||||
|
||||
sandboxToken, expiresAt, err := h.createSession(sandboxUser.ID)
|
||||
if err != nil {
|
||||
respondError(c, http.StatusInternalServerError, "session_creation_failed", "failed to create sandbox session")
|
||||
return
|
||||
}
|
||||
|
||||
slog.Info("admin assume sandbox",
|
||||
"event", "admin_assume",
|
||||
"actor_user_id", adminUser.ID,
|
||||
"actor_email", adminUser.Email,
|
||||
"target_user_id", sandboxUser.ID,
|
||||
"target_email", sandboxUser.Email,
|
||||
)
|
||||
|
||||
// NOTE: cookies are intentionally NOT set by accounts.svc.plus.
|
||||
// The console BFF is responsible for setting host-scoped cookies.
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"ok": true,
|
||||
"assumed": assumeSandboxEmail,
|
||||
"token": sandboxToken,
|
||||
"expiresAt": expiresAt.UTC(),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *handler) adminAssumeRevert(c *gin.Context) {
|
||||
adminUser, ok := h.requireAdminPermission(c, permissionAdminSettingsWrite)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !h.isRootAccount(adminUser) {
|
||||
respondError(c, http.StatusForbidden, "root_only", "root only")
|
||||
return
|
||||
}
|
||||
|
||||
slog.Info("admin assume revert",
|
||||
"event", "admin_assume_revert",
|
||||
"actor_user_id", adminUser.ID,
|
||||
"actor_email", adminUser.Email,
|
||||
"target_email", assumeSandboxEmail,
|
||||
)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
func (h *handler) adminAssumeStatus(c *gin.Context) {
|
||||
adminUser, ok := h.requireAdminPermission(c, permissionAdminSettingsRead)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !h.isRootAccount(adminUser) {
|
||||
respondError(c, http.StatusForbidden, "root_only", "root only")
|
||||
return
|
||||
}
|
||||
|
||||
// NOTE: assume status is tracked via host-scoped cookies owned by console.svc.plus.
|
||||
// accounts.svc.plus cannot observe that state safely, so we only expose a stub.
|
||||
c.JSON(http.StatusOK, gin.H{"isAssuming": false, "target": ""})
|
||||
}
|
||||
|
||||
// guard unused imports if build tags change.
|
||||
var _ = context.Background
|
||||
@ -86,14 +86,8 @@ func (h *handler) bindSandboxNode(c *gin.Context) {
|
||||
|
||||
// Update the in-memory registry if available
|
||||
if h.agentRegistry != nil {
|
||||
// This should ideally be handled by a more robust event system,
|
||||
// but since it's a single instance (usually), we can just clear and reset.
|
||||
// For now, let's assume the registry will reload if we trigger it or we just set it here.
|
||||
// Wait, Registry needs to know ALL sandbox agents.
|
||||
// I'll update the registry's internal state.
|
||||
|
||||
// First reset all sandbox flags (not supported by current Registry API, let's just set the new one)
|
||||
// TODO: Implement ClearSandboxAgents in Registry
|
||||
// Enforce 1-to-1 binding: clear then set.
|
||||
h.agentRegistry.ClearSandboxAgents()
|
||||
if agentID != "" {
|
||||
h.agentRegistry.SetSandboxAgent(agentID, true)
|
||||
}
|
||||
|
||||
174
api/agent_server.go
Normal file
174
api/agent_server.go
Normal file
@ -0,0 +1,174 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"account/internal/agentproto"
|
||||
"account/internal/agentserver"
|
||||
"account/internal/store"
|
||||
"account/internal/xrayconfig"
|
||||
)
|
||||
|
||||
const agentIDHeader = "X-Agent-ID"
|
||||
|
||||
func (h *handler) listAgentUsers(c *gin.Context) {
|
||||
if h.agentRegistry == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "agent_registry_unavailable"})
|
||||
return
|
||||
}
|
||||
|
||||
token := extractToken(c.GetHeader("Authorization"))
|
||||
if token == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing_token"})
|
||||
return
|
||||
}
|
||||
|
||||
credIdentity, ok := h.agentRegistry.Authenticate(token)
|
||||
if !ok || credIdentity == nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid_token"})
|
||||
return
|
||||
}
|
||||
|
||||
agentID := strings.TrimSpace(c.GetHeader(agentIDHeader))
|
||||
if agentID == "" {
|
||||
agentID = strings.TrimSpace(c.Query("agentId"))
|
||||
}
|
||||
if agentID == "" {
|
||||
agentID = credIdentity.ID
|
||||
}
|
||||
|
||||
identity := *credIdentity
|
||||
if agentID != "" && agentID != identity.ID {
|
||||
// Shared token scenario: register a concrete agent id so sandbox bindings can target it.
|
||||
identity = h.agentRegistry.RegisterAgent(agentID, identity.Groups)
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
clients := make([]xrayconfig.Client, 0, 1)
|
||||
|
||||
if h.agentRegistry.IsSandboxAgent(identity.ID) {
|
||||
sandboxUser, err := h.store.GetUserByEmail(c.Request.Context(), sandboxUserEmail)
|
||||
if err != nil {
|
||||
if err == store.ErrUserNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "sandbox_missing"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "sandbox_lookup_failed"})
|
||||
return
|
||||
}
|
||||
if err := h.ensureSandboxProxyUUID(c.Request.Context(), sandboxUser); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "sandbox_uuid_rotation_failed"})
|
||||
return
|
||||
}
|
||||
|
||||
uuid := strings.TrimSpace(sandboxUser.ProxyUUID)
|
||||
if uuid == "" {
|
||||
uuid = strings.TrimSpace(sandboxUser.ID)
|
||||
}
|
||||
if uuid != "" {
|
||||
clients = append(clients, xrayconfig.Client{
|
||||
ID: uuid,
|
||||
Email: sandboxUserEmail,
|
||||
Flow: xrayconfig.DefaultFlow,
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, agentproto.ClientListResponse{
|
||||
Clients: clients,
|
||||
Total: len(clients),
|
||||
GeneratedAt: now,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
users, err := h.store.ListUsers(c.Request.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "list_users_failed"})
|
||||
return
|
||||
}
|
||||
|
||||
for _, u := range users {
|
||||
email := strings.ToLower(strings.TrimSpace(u.Email))
|
||||
if email == sandboxUserEmail || email == "demo@svc.plus" {
|
||||
continue
|
||||
}
|
||||
if !u.Active {
|
||||
continue
|
||||
}
|
||||
if !u.EmailVerified {
|
||||
continue
|
||||
}
|
||||
if u.ProxyUUIDExpiresAt != nil && now.After(*u.ProxyUUIDExpiresAt) {
|
||||
continue
|
||||
}
|
||||
|
||||
id := strings.TrimSpace(u.ProxyUUID)
|
||||
if id == "" {
|
||||
id = strings.TrimSpace(u.ID)
|
||||
}
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
clients = append(clients, xrayconfig.Client{
|
||||
ID: id,
|
||||
Email: strings.TrimSpace(u.Email),
|
||||
Flow: xrayconfig.DefaultFlow,
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, agentproto.ClientListResponse{
|
||||
Clients: clients,
|
||||
Total: len(clients),
|
||||
GeneratedAt: now,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *handler) reportAgentStatus(c *gin.Context) {
|
||||
if h.agentRegistry == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "agent_registry_unavailable"})
|
||||
return
|
||||
}
|
||||
|
||||
token := extractToken(c.GetHeader("Authorization"))
|
||||
if token == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing_token"})
|
||||
return
|
||||
}
|
||||
|
||||
credIdentity, ok := h.agentRegistry.Authenticate(token)
|
||||
if !ok || credIdentity == nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid_token"})
|
||||
return
|
||||
}
|
||||
|
||||
var report agentproto.StatusReport
|
||||
if err := c.ShouldBindJSON(&report); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request", "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
agentID := strings.TrimSpace(report.AgentID)
|
||||
if agentID == "" {
|
||||
agentID = strings.TrimSpace(c.GetHeader(agentIDHeader))
|
||||
}
|
||||
if agentID == "" {
|
||||
agentID = credIdentity.ID
|
||||
}
|
||||
|
||||
identity := *credIdentity
|
||||
if agentID != "" && agentID != identity.ID {
|
||||
identity = h.agentRegistry.RegisterAgent(agentID, identity.Groups)
|
||||
}
|
||||
|
||||
// Ensure report uses the resolved agent id.
|
||||
report.AgentID = identity.ID
|
||||
h.agentRegistry.ReportStatus(identity, report)
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
var _ = agentserver.Identity{}
|
||||
30
api/api.go
30
api/api.go
@ -315,6 +315,18 @@ func RegisterRoutes(r *gin.Engine, opts ...Option) {
|
||||
authProtected.POST("/admin/blacklist", h.addToBlacklist)
|
||||
authProtected.DELETE("/admin/blacklist/:email", h.removeFromBlacklist)
|
||||
|
||||
// Sandbox node binding (root-only via permissions guard).
|
||||
authProtected.GET("/admin/sandbox/binding", h.getSandboxBinding)
|
||||
authProtected.POST("/admin/sandbox/bind", h.bindSandboxNode)
|
||||
|
||||
// Public read of sandbox binding for demo/sandbox user experience.
|
||||
authProtected.GET("/sandbox/binding", h.getSandboxBindingPublic)
|
||||
|
||||
// Root-only identity switch to sandbox@svc.plus (hard-coded allowlist).
|
||||
authProtected.POST("/admin/assume", h.adminAssume)
|
||||
authProtected.POST("/admin/assume/revert", h.adminAssumeRevert)
|
||||
authProtected.GET("/admin/assume/status", h.adminAssumeStatus)
|
||||
|
||||
authProtected.GET("/users", h.listUsers)
|
||||
|
||||
// Internal routes for service-to-service reads.
|
||||
@ -335,6 +347,8 @@ func RegisterRoutes(r *gin.Engine, opts ...Option) {
|
||||
// stay outside token middleware to support dashboard session tokens.
|
||||
agentServerGroup := r.Group("/api/agent-server/v1")
|
||||
agentServerGroup.GET("/nodes", h.listAgentNodes)
|
||||
agentServerGroup.GET("/users", h.listAgentUsers)
|
||||
agentServerGroup.POST("/status", h.reportAgentStatus)
|
||||
|
||||
// Legacy alias kept for backward compatibility.
|
||||
agentGroup := r.Group("/api/agent")
|
||||
@ -992,6 +1006,13 @@ func (h *handler) login(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Sandbox user is not allowed to login by password/totp.
|
||||
// Root can only assume into sandbox via the admin assume endpoint.
|
||||
if strings.EqualFold(strings.TrimSpace(user.Email), sandboxUserEmail) {
|
||||
respondError(c, http.StatusForbidden, "sandbox_no_login", "sandbox login is disabled")
|
||||
return
|
||||
}
|
||||
|
||||
if password != "" {
|
||||
if bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)) != nil {
|
||||
respondError(c, http.StatusUnauthorized, "invalid_credentials", "invalid credentials")
|
||||
@ -1217,6 +1238,11 @@ func (h *handler) session(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Sandbox UUID rotates hourly; refresh on session reads so the UI always sees a valid UUID.
|
||||
if err := h.ensureSandboxProxyUUID(c.Request.Context(), user); err != nil {
|
||||
slog.Warn("failed to rotate sandbox proxy uuid", "err", err, "userID", user.ID)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"user": sanitizeUser(user, nil)})
|
||||
}
|
||||
|
||||
@ -2658,7 +2684,9 @@ func (h *handler) isReadOnlyAccount(user *store.User) bool {
|
||||
}
|
||||
name := strings.TrimSpace(user.Name)
|
||||
email := strings.TrimSpace(user.Email)
|
||||
if strings.EqualFold(name, "demo") || strings.EqualFold(email, "demo@svc.plus") {
|
||||
if strings.EqualFold(name, "demo") ||
|
||||
strings.EqualFold(email, "demo@svc.plus") ||
|
||||
strings.EqualFold(email, sandboxUserEmail) {
|
||||
return true
|
||||
}
|
||||
for _, group := range user.Groups {
|
||||
|
||||
40
api/sandbox_binding_public.go
Normal file
40
api/sandbox_binding_public.go
Normal file
@ -0,0 +1,40 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"account/internal/model"
|
||||
)
|
||||
|
||||
// getSandboxBindingPublic returns the currently bound sandbox agent (if any).
|
||||
// It is intentionally readable by any authenticated user so demo/sandbox users
|
||||
// do not depend on localStorage browser state.
|
||||
func (h *handler) getSandboxBindingPublic(c *gin.Context) {
|
||||
if _, ok := h.requireAuthenticatedUser(c); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if h.db == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "database_not_configured"})
|
||||
return
|
||||
}
|
||||
|
||||
var binding model.SandboxBinding
|
||||
if err := h.db.WithContext(c.Request.Context()).First(&binding).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusOK, gin.H{"address": "", "updatedAt": int64(0)})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed_to_query_binding", "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"address": binding.AgentID,
|
||||
"updatedAt": binding.UpdatedAt.UnixMilli(),
|
||||
})
|
||||
}
|
||||
40
api/sandbox_uuid.go
Normal file
40
api/sandbox_uuid.go
Normal file
@ -0,0 +1,40 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"account/internal/store"
|
||||
)
|
||||
|
||||
const (
|
||||
sandboxUserEmail = "sandbox@svc.plus"
|
||||
sandboxUUIDRotationWindow = time.Hour
|
||||
)
|
||||
|
||||
// ensureSandboxProxyUUID enforces hourly rotation of the sandbox user's ProxyUUID.
|
||||
// It is intentionally strict: only the hard-coded sandbox email is eligible.
|
||||
func (h *handler) ensureSandboxProxyUUID(ctx context.Context, user *store.User) error {
|
||||
if h == nil || user == nil {
|
||||
return nil
|
||||
}
|
||||
email := strings.ToLower(strings.TrimSpace(user.Email))
|
||||
if email != sandboxUserEmail {
|
||||
return nil
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
needsRotation := strings.TrimSpace(user.ProxyUUID) == "" ||
|
||||
user.ProxyUUIDExpiresAt == nil ||
|
||||
!now.Before(*user.ProxyUUIDExpiresAt)
|
||||
|
||||
if !needsRotation {
|
||||
return nil
|
||||
}
|
||||
|
||||
exp := now.Add(sandboxUUIDRotationWindow)
|
||||
user.ProxyUUID = generateRandomUUID()
|
||||
user.ProxyUUIDExpiresAt = &exp
|
||||
return h.store.UpdateUser(ctx, user)
|
||||
}
|
||||
@ -63,11 +63,23 @@ func (h *handler) listAgentNodes(c *gin.Context) {
|
||||
}
|
||||
|
||||
if user.ProxyUUIDExpiresAt != nil && time.Now().UTC().After(*user.ProxyUUIDExpiresAt) {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "proxy_uuid_expired",
|
||||
"message": "proxy access has expired, please renew",
|
||||
})
|
||||
return
|
||||
// Sandbox rotates hourly; never block it on expiry.
|
||||
if strings.EqualFold(strings.TrimSpace(user.Email), sandboxUserEmail) {
|
||||
if err := h.ensureSandboxProxyUUID(c.Request.Context(), user); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "sandbox_uuid_rotation_failed"})
|
||||
return
|
||||
}
|
||||
proxyUUID = strings.TrimSpace(user.ProxyUUID)
|
||||
if proxyUUID == "" {
|
||||
proxyUUID = user.ID
|
||||
}
|
||||
} else {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "proxy_uuid_expired",
|
||||
"message": "proxy access has expired, please renew",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Add panic recovery for this handler
|
||||
|
||||
@ -22,6 +22,7 @@ type ClientOptions struct {
|
||||
Timeout time.Duration
|
||||
InsecureSkipVerify bool
|
||||
UserAgent string
|
||||
AgentID string
|
||||
}
|
||||
|
||||
// Client issues authenticated requests against the controller.
|
||||
@ -30,6 +31,7 @@ type Client struct {
|
||||
token string
|
||||
http *http.Client
|
||||
userAgent string
|
||||
agentID string
|
||||
}
|
||||
|
||||
// NewClient constructs a client for the provided controller URL and token.
|
||||
@ -73,12 +75,14 @@ func NewClient(baseURL, token string, opts ClientOptions) (*Client, error) {
|
||||
if userAgent == "" {
|
||||
userAgent = "xcontrol-agent"
|
||||
}
|
||||
agentID := strings.TrimSpace(opts.AgentID)
|
||||
|
||||
return &Client{
|
||||
baseURL: parsed,
|
||||
token: token,
|
||||
http: client,
|
||||
userAgent: userAgent,
|
||||
agentID: agentID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -148,4 +152,7 @@ func (c *Client) ReportStatus(ctx context.Context, report agentproto.StatusRepor
|
||||
func (c *Client) applyHeaders(req *http.Request) {
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
req.Header.Set("User-Agent", c.userAgent)
|
||||
if c.agentID != "" {
|
||||
req.Header.Set("X-Agent-ID", c.agentID)
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,6 +70,7 @@ func Run(ctx context.Context, opts Options) error {
|
||||
Timeout: httpTimeout,
|
||||
InsecureSkipVerify: opts.Agent.TLS.InsecureSkipVerify,
|
||||
UserAgent: buildUserAgent(opts.Agent.ID),
|
||||
AgentID: opts.Agent.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@ -146,7 +147,7 @@ func Run(ctx context.Context, opts Options) error {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
runStatusReporter(reporterCtx, client, tracker, statusInterval, syncInterval, logger)
|
||||
runStatusReporter(reporterCtx, client, tracker, statusInterval, syncInterval, opts.Agent.ID, logger)
|
||||
}()
|
||||
|
||||
<-ctx.Done()
|
||||
@ -163,13 +164,13 @@ func buildUserAgent(id string) string {
|
||||
return fmt.Sprintf("xcontrol-agent/%s", id)
|
||||
}
|
||||
|
||||
func runStatusReporter(ctx context.Context, client *Client, tracker *syncTracker, interval, syncInterval time.Duration, logger *slog.Logger) {
|
||||
func runStatusReporter(ctx context.Context, client *Client, tracker *syncTracker, interval, syncInterval time.Duration, agentID string, logger *slog.Logger) {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
send := func() {
|
||||
snapshot := tracker.Snapshot()
|
||||
report := buildStatusReport(snapshot, syncInterval)
|
||||
report := buildStatusReport(snapshot, syncInterval, agentID)
|
||||
if err := client.ReportStatus(ctx, report); err != nil {
|
||||
logger.Warn("failed to report agent status", "err", err)
|
||||
}
|
||||
@ -187,7 +188,7 @@ func runStatusReporter(ctx context.Context, client *Client, tracker *syncTracker
|
||||
}
|
||||
}
|
||||
|
||||
func buildStatusReport(snapshot trackerSnapshot, syncInterval time.Duration) agentproto.StatusReport {
|
||||
func buildStatusReport(snapshot trackerSnapshot, syncInterval time.Duration, agentID string) agentproto.StatusReport {
|
||||
healthy := snapshot.LastError == "" && !snapshot.LastSuccess.IsZero()
|
||||
|
||||
running := false
|
||||
@ -199,6 +200,7 @@ func buildStatusReport(snapshot trackerSnapshot, syncInterval time.Duration) age
|
||||
}
|
||||
|
||||
report := agentproto.StatusReport{
|
||||
AgentID: strings.TrimSpace(agentID),
|
||||
Healthy: healthy,
|
||||
Message: snapshot.LastError,
|
||||
Users: snapshot.Clients,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user