yao/openapi/nodes/nodes.go
Max 42b45f9357 refactor(sandbox): streamline sandbox configuration loading and enhance local execution capabilities
- Replaced the previous file extension checks with a unified parsing function for sandbox configuration, improving code clarity and maintainability.
- Introduced a new HostExecConfig structure to manage local execution settings, allowing for more granular control over command execution permissions.
- Removed deprecated Moapi API files and related functionality, simplifying the codebase and reducing maintenance overhead.
- Updated the Tai node registration process to ensure local capabilities are accurately reflected based on the environment, enhancing overall system robustness.
2026-03-18 11:22:08 +08:00

135 lines
3.8 KiB
Go

package nodes
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/yaoapp/yao/openapi/oauth/authorized"
"github.com/yaoapp/yao/openapi/oauth/types"
"github.com/yaoapp/yao/openapi/response"
"github.com/yaoapp/yao/tai/registry"
taitypes "github.com/yaoapp/yao/tai/types"
)
// Attach registers Tai node endpoints on the given group.
// - GET / — list nodes (filtered by team/user from token)
// - GET /:id — get single node (owner check)
func Attach(group *gin.RouterGroup, oauth types.OAuth) {
group.Use(oauth.Guard)
group.GET("", handleList)
group.GET("/:id", handleGet)
}
type nodeResponse struct {
TaiID string `json:"tai_id"`
MachineID string `json:"machine_id,omitempty"`
Version string `json:"version,omitempty"`
DisplayName string `json:"display_name,omitempty"`
Mode string `json:"mode"`
Addr string `json:"addr,omitempty"`
Status string `json:"status"`
System systemResponse `json:"system"`
Capabilities map[string]bool `json:"capabilities,omitempty"`
Ports map[string]int `json:"ports,omitempty"`
ConnectedAt *time.Time `json:"connected_at,omitempty"`
LastPing *time.Time `json:"last_ping,omitempty"`
}
type systemResponse struct {
OS string `json:"os"`
Arch string `json:"arch"`
Hostname string `json:"hostname"`
NumCPU int `json:"num_cpu"`
TotalMem int64 `json:"total_mem,omitempty"`
Shell string `json:"shell,omitempty"`
}
func snapToResponse(s taitypes.NodeMeta) nodeResponse {
r := nodeResponse{
TaiID: s.TaiID,
MachineID: s.MachineID,
Version: s.Version,
DisplayName: s.DisplayName,
Mode: s.Mode,
Addr: s.Addr,
Status: s.Status,
Capabilities: map[string]bool{"docker": s.Capabilities.Docker, "k8s": s.Capabilities.K8s, "host_exec": s.Capabilities.HostExec},
Ports: map[string]int{"grpc": s.Ports.GRPC, "http": s.Ports.HTTP, "vnc": s.Ports.VNC, "docker": s.Ports.Docker, "k8s": s.Ports.K8s},
System: systemResponse{
OS: s.System.OS,
Arch: s.System.Arch,
Hostname: s.System.Hostname,
NumCPU: s.System.NumCPU,
TotalMem: s.System.TotalMem,
Shell: s.System.Shell,
},
}
if !s.ConnectedAt.IsZero() {
r.ConnectedAt = &s.ConnectedAt
}
if !s.LastPing.IsZero() {
r.LastPing = &s.LastPing
}
return r
}
// nodeOwnedBy checks whether a node belongs to the caller.
// TeamID match → true; no team and UserID match → true.
func nodeOwnedBy(snap *taitypes.NodeMeta, authInfo *types.AuthorizedInfo) bool {
if authInfo == nil {
return true
}
if authInfo.TeamID != "" {
return snap.Auth.TeamID == authInfo.TeamID
}
if authInfo.UserID != "" {
return snap.Auth.TeamID == "" && snap.Auth.UserID == authInfo.UserID
}
return true
}
func handleList(c *gin.Context) {
reg := registry.Global()
if reg == nil {
response.RespondWithSuccess(c, http.StatusOK, []nodeResponse{})
return
}
authInfo := authorized.GetInfo(c)
snaps := reg.List()
result := make([]nodeResponse, 0, len(snaps))
for i := range snaps {
s := &snaps[i]
if s.Mode != "local" && !nodeOwnedBy(s, authInfo) {
continue
}
result = append(result, snapToResponse(*s))
}
response.RespondWithSuccess(c, http.StatusOK, result)
}
func handleGet(c *gin.Context) {
reg := registry.Global()
if reg == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "node registry not available"})
return
}
id := c.Param("id")
snap, ok := reg.Get(id)
if !ok {
c.JSON(http.StatusNotFound, gin.H{"error": "node not found"})
return
}
authInfo := authorized.GetInfo(c)
if snap.Mode != "local" && !nodeOwnedBy(snap, authInfo) {
c.JSON(http.StatusForbidden, gin.H{"error": "no permission to access this node"})
return
}
response.RespondWithSuccess(c, http.StatusOK, snapToResponse(*snap))
}