Implements the hypervisor design's Phase 1: a second runtime.Runtime backend (QEMU) that runs each service component as a Nanos unikernel VM instead of a podman container, selected per-component via a new runtime = "unikernel" service-def field. - internal/runtime/qemu.go: QEMURuntime. Pull extracts the ELF from the OCI image; Run does `ops build` + boots qemu-system-x86_64 with KVM, user-mode net port-forwards, QMP control socket and serial console log; Stop/Remove/Inspect/List/Logs map onto VM lifecycle + state dir. - proto/registry/servicedef: add runtime, memory_mb, vcpus fields (registry migration 5). - agent: holds both runtimes; runtimeFor() selects per component; listAllContainers() merges containers + VMs so drift/status see both. Unikernel runtime auto-enables on nodes with /dev/kvm + ops. Validated end-to-end on straylight: a test service deploys via `mcp deploy --direct`, boots as a Nanos unikernel, serves HTTP through the agent port-forward, and reports running via `mcp status`/`mcp logs`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
93 lines
2.6 KiB
Go
93 lines
2.6 KiB
Go
package agent
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"io"
|
|
"os/exec"
|
|
|
|
mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1"
|
|
"git.wntrmute.dev/mc/mcp/internal/registry"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
// logStreamer is implemented by both the Podman (container logs) and QEMU
|
|
// (serial console) runtimes.
|
|
type logStreamer interface {
|
|
Logs(ctx context.Context, name string, tail int, follow, timestamps bool, since string) *exec.Cmd
|
|
}
|
|
|
|
// Logs streams container logs for a service component.
|
|
func (a *Agent) Logs(req *mcpv1.LogsRequest, stream mcpv1.McpAgentService_LogsServer) error {
|
|
if req.GetService() == "" {
|
|
return status.Error(codes.InvalidArgument, "service name is required")
|
|
}
|
|
|
|
// Resolve component name.
|
|
component := req.GetComponent()
|
|
if component == "" {
|
|
components, err := registry.ListComponents(a.DB, req.GetService())
|
|
if err != nil {
|
|
return status.Errorf(codes.Internal, "list components: %v", err)
|
|
}
|
|
if len(components) == 0 {
|
|
return status.Error(codes.NotFound, "no components found for service")
|
|
}
|
|
component = components[0].Name
|
|
}
|
|
|
|
containerName := ContainerNameFor(req.GetService(), component)
|
|
|
|
// Select the runtime for this component (container vs unikernel) and
|
|
// stream its logs (podman logs / journald, or the VM serial console).
|
|
var compRuntime string
|
|
if c, err := registry.GetComponent(a.DB, req.GetService(), component); err == nil {
|
|
compRuntime = c.Runtime
|
|
}
|
|
ls, ok := a.runtimeFor(compRuntime).(logStreamer)
|
|
if !ok {
|
|
return status.Error(codes.Internal, "selected runtime does not support log streaming")
|
|
}
|
|
|
|
cmd := ls.Logs(stream.Context(), containerName, int(req.GetTail()), req.GetFollow(), req.GetTimestamps(), req.GetSince())
|
|
|
|
a.Logger.Info("streaming logs", "container", containerName, "runtime", compRuntime, "args", cmd.Args)
|
|
|
|
// Podman writes container stdout to its stdout and container stderr
|
|
// to its stderr. Merge both into a single pipe.
|
|
pr, pw := io.Pipe()
|
|
cmd.Stdout = pw
|
|
cmd.Stderr = pw
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
_ = pw.Close()
|
|
return status.Errorf(codes.Internal, "start podman logs: %v", err)
|
|
}
|
|
|
|
// Close the write end when the command exits so the scanner finishes.
|
|
go func() {
|
|
err := cmd.Wait()
|
|
if err != nil {
|
|
a.Logger.Warn("podman logs exited", "container", containerName, "error", err)
|
|
}
|
|
_ = pw.Close()
|
|
}()
|
|
|
|
scanner := bufio.NewScanner(pr)
|
|
for scanner.Scan() {
|
|
line := scanner.Bytes()
|
|
if len(line) == 0 {
|
|
continue
|
|
}
|
|
if err := stream.Send(&mcpv1.LogsResponse{
|
|
Data: append(line, '\n'),
|
|
}); err != nil {
|
|
_ = cmd.Process.Kill()
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|