Skip to content

Commit 9dfa4c7

Browse files
committed
api: implement dedicated stat endpoint, remove from cp handler
- Add StatInstancePath handler in instances.go - Connect to guest agent StatPath gRPC for path info - Return proper 409 if instance not running - Remove stat direction from cp WebSocket handler - Remove CpStatResponse type (now using generated PathInfo) - Regenerate oapi from updated openapi.yaml
1 parent 1953048 commit 9dfa4c7

File tree

3 files changed

+513
-146
lines changed

3 files changed

+513
-146
lines changed

cmd/api/api/cp.go

Lines changed: 2 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717

1818
// CpRequest represents the JSON body for copy requests
1919
type CpRequest struct {
20-
// Direction: "to" copies from client to guest, "from" copies from guest to client, "stat" queries path info
20+
// Direction: "to" copies from client to guest, "from" copies from guest to client
2121
Direction string `json:"direction"`
2222
// Path in the guest filesystem
2323
GuestPath string `json:"guest_path"`
@@ -35,19 +35,6 @@ type CpRequest struct {
3535
Gid uint32 `json:"gid,omitempty"`
3636
}
3737

38-
// CpStatResponse contains information about a path in the guest
39-
type CpStatResponse struct {
40-
Type string `json:"type"` // "stat"
41-
Exists bool `json:"exists"`
42-
IsDir bool `json:"is_dir"`
43-
IsFile bool `json:"is_file"`
44-
IsSymlink bool `json:"is_symlink,omitempty"`
45-
LinkTarget string `json:"link_target,omitempty"`
46-
Mode uint32 `json:"mode"`
47-
Size int64 `json:"size"`
48-
Error string `json:"error,omitempty"`
49-
}
50-
5138
// CpFileHeader is sent before file data in WebSocket protocol
5239
type CpFileHeader struct {
5340
Type string `json:"type"` // "header"
@@ -152,10 +139,8 @@ func (s *ApiService) CpHandler(w http.ResponseWriter, r *http.Request) {
152139
cpErr = s.handleCopyTo(ctx, ws, inst, cpReq)
153140
case "from":
154141
cpErr = s.handleCopyFrom(ctx, ws, inst, cpReq)
155-
case "stat":
156-
cpErr = s.handleStat(ctx, ws, inst, cpReq)
157142
default:
158-
cpErr = fmt.Errorf("invalid direction: %s (must be 'to', 'from', or 'stat')", cpReq.Direction)
143+
cpErr = fmt.Errorf("invalid direction: %s (must be 'to' or 'from')", cpReq.Direction)
159144
}
160145

161146
duration := time.Since(startTime)
@@ -358,34 +343,3 @@ func (s *ApiService) handleCopyFrom(ctx context.Context, ws *websocket.Conn, ins
358343
return nil
359344
}
360345

361-
// handleStat returns information about a path in the guest
362-
func (s *ApiService) handleStat(ctx context.Context, ws *websocket.Conn, inst *instances.Instance, req CpRequest) error {
363-
grpcConn, err := guest.GetOrCreateConnPublic(ctx, inst.VsockSocket)
364-
if err != nil {
365-
return fmt.Errorf("get grpc connection: %w", err)
366-
}
367-
368-
client := guest.NewGuestServiceClient(grpcConn)
369-
resp, err := client.StatPath(ctx, &guest.StatPathRequest{
370-
Path: req.GuestPath,
371-
FollowLinks: req.FollowLinks,
372-
})
373-
if err != nil {
374-
return fmt.Errorf("stat path: %w", err)
375-
}
376-
377-
statResp := CpStatResponse{
378-
Type: "stat",
379-
Exists: resp.Exists,
380-
IsDir: resp.IsDir,
381-
IsFile: resp.IsFile,
382-
IsSymlink: resp.IsSymlink,
383-
LinkTarget: resp.LinkTarget,
384-
Mode: resp.Mode,
385-
Size: resp.Size,
386-
Error: resp.Error,
387-
}
388-
respJSON, _ := json.Marshal(statResp)
389-
return ws.WriteMessage(websocket.TextMessage, respJSON)
390-
}
391-

cmd/api/api/instances.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"net/http"
99

1010
"github.com/c2h5oh/datasize"
11+
"github.com/onkernel/hypeman/lib/guest"
1112
"github.com/onkernel/hypeman/lib/instances"
1213
"github.com/onkernel/hypeman/lib/logger"
1314
mw "github.com/onkernel/hypeman/lib/middleware"
@@ -430,6 +431,68 @@ func (s *ApiService) GetInstanceLogs(ctx context.Context, request oapi.GetInstan
430431
return logsStreamResponse{logChan: logChan}, nil
431432
}
432433

434+
// StatInstancePath returns information about a path in the guest filesystem
435+
// The id parameter can be an instance ID, name, or ID prefix
436+
// Note: Resolution is handled by ResolveResource middleware
437+
func (s *ApiService) StatInstancePath(ctx context.Context, request oapi.StatInstancePathRequestObject) (oapi.StatInstancePathResponseObject, error) {
438+
log := logger.FromContext(ctx)
439+
440+
inst := mw.GetResolvedInstance[instances.Instance](ctx)
441+
if inst == nil {
442+
return oapi.StatInstancePath500JSONResponse{
443+
Code: "internal_error",
444+
Message: "resource not resolved",
445+
}, nil
446+
}
447+
448+
if inst.State != instances.StateRunning {
449+
return oapi.StatInstancePath409JSONResponse{
450+
Code: "invalid_state",
451+
Message: fmt.Sprintf("instance must be running (current state: %s)", inst.State),
452+
}, nil
453+
}
454+
455+
// Connect to guest agent
456+
grpcConn, err := guest.GetOrCreateConnPublic(ctx, inst.VsockSocket)
457+
if err != nil {
458+
log.ErrorContext(ctx, "failed to get grpc connection", "error", err)
459+
return oapi.StatInstancePath500JSONResponse{
460+
Code: "internal_error",
461+
Message: "failed to connect to guest agent",
462+
}, nil
463+
}
464+
465+
client := guest.NewGuestServiceClient(grpcConn)
466+
followLinks := false
467+
if request.Params.FollowLinks != nil {
468+
followLinks = *request.Params.FollowLinks
469+
}
470+
471+
resp, err := client.StatPath(ctx, &guest.StatPathRequest{
472+
Path: request.Params.Path,
473+
FollowLinks: followLinks,
474+
})
475+
if err != nil {
476+
log.ErrorContext(ctx, "stat path failed", "error", err, "path", request.Params.Path)
477+
return oapi.StatInstancePath500JSONResponse{
478+
Code: "internal_error",
479+
Message: "failed to stat path in guest",
480+
}, nil
481+
}
482+
483+
// Convert types from protobuf to OAPI
484+
mode := int(resp.Mode)
485+
return oapi.StatInstancePath200JSONResponse{
486+
Exists: resp.Exists,
487+
IsDir: &resp.IsDir,
488+
IsFile: &resp.IsFile,
489+
IsSymlink: &resp.IsSymlink,
490+
LinkTarget: &resp.LinkTarget,
491+
Mode: &mode,
492+
Size: &resp.Size,
493+
}, nil
494+
}
495+
433496
// AttachVolume attaches a volume to an instance (not yet implemented)
434497
func (s *ApiService) AttachVolume(ctx context.Context, request oapi.AttachVolumeRequestObject) (oapi.AttachVolumeResponseObject, error) {
435498
return oapi.AttachVolume500JSONResponse{

0 commit comments

Comments
 (0)