yao/openapi/nodes/nodes.go
Max 7cccf62841 feat(tai): refactor Dial* functions and remove requireKubeConfig hard-fail
- Consolidate DialRemote/DialTunnel common logic into buildResources + dialEnv interface
- Remove strict capability check that prevented ConnResources creation for host-exec-only nodes
- Merge gRPC-discovered capabilities with registration-declared capabilities in DialTunnel
- Replace requireKubeConfig hard-fail with graceful skip when kubeconfig is absent
- Introduce tai/types package for shared Ports/Capabilities/SystemInfo/AuthInfo/NodeMeta
- Add tai/conn.go (ConnResources) and tai/dial.go (DialRemote/DialTunnel/DialLocal)
- Rename tai/sandbox → tai/runtime for clarity
- Update sandbox/v2, workspace, agent/sandbox/v2 test utilities for build-tag isolation

Made-with: Cursor
2026-03-12 15:44:24 +08:00

139 lines
3.9 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)
var snaps []taitypes.NodeMeta
if authInfo != nil && authInfo.TeamID != "" {
snaps = reg.ListByTeam(authInfo.TeamID)
} else if authInfo != nil && authInfo.UserID != "" {
snaps = reg.ListByUser(authInfo.UserID)
} else {
snaps = reg.List()
}
result := make([]nodeResponse, 0, len(snaps))
for _, s := range snaps {
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 !nodeOwnedBy(snap, authInfo) {
c.JSON(http.StatusForbidden, gin.H{"error": "no permission to access this node"})
return
}
response.RespondWithSuccess(c, http.StatusOK, snapToResponse(*snap))
}