Add host agent RPC server with sandbox lifecycle management
Implement the host agent as a Connect RPC server that orchestrates sandbox creation, destruction, pause/resume, and command execution. Includes sandbox manager with TTL-based reaper, network slot allocator, rootfs cloning, hostagent proto definition with generated stubs, and test/debug scripts. Fix Firecracker process lifetime bug where VM was tied to HTTP request context instead of background context.
This commit is contained in:
@ -770,8 +770,8 @@ open http://localhost:8000/admin/
|
||||
1. Write `internal/network/` — TAP + NAT per sandbox
|
||||
2. Write `internal/filesystem/` — CoW rootfs clones
|
||||
3. Define hostagent.proto, generate stubs
|
||||
4. Write host agent gRPC server
|
||||
5. Test: grpcurl to create/exec/destroy
|
||||
4. Write host agent rpc server
|
||||
5. Test: curl to create/exec/destroy
|
||||
|
||||
### Phase 3: Control Plane
|
||||
1. Set up PostgreSQL, write goose migrations
|
||||
|
||||
3
Makefile
3
Makefile
@ -21,7 +21,7 @@ build-agent:
|
||||
|
||||
build-envd:
|
||||
cd $(ENVD_DIR) && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
||||
go build -ldflags="$(LDFLAGS)" -o ../$(GOBIN)/envd .
|
||||
go build -ldflags="$(LDFLAGS)" -o $(GOBIN)/envd .
|
||||
@file $(GOBIN)/envd | grep -q "statically linked" || \
|
||||
(echo "ERROR: envd is not statically linked!" && exit 1)
|
||||
|
||||
@ -85,6 +85,7 @@ generate: proto sqlc
|
||||
|
||||
proto:
|
||||
cd proto/envd && buf generate
|
||||
cd proto/hostagent && buf generate
|
||||
cd $(ENVD_DIR)/spec && buf generate
|
||||
|
||||
sqlc:
|
||||
|
||||
@ -2,26 +2,16 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.omukk.dev/wrenn/sandbox/internal/envdclient"
|
||||
"git.omukk.dev/wrenn/sandbox/internal/network"
|
||||
"git.omukk.dev/wrenn/sandbox/internal/vm"
|
||||
)
|
||||
|
||||
const (
|
||||
kernelPath = "/var/lib/wrenn/kernels/vmlinux"
|
||||
baseRootfs = "/var/lib/wrenn/sandboxes/rootfs.ext4"
|
||||
sandboxesDir = "/var/lib/wrenn/sandboxes"
|
||||
sandboxID = "sb-demo0001"
|
||||
slotIndex = 1
|
||||
"git.omukk.dev/wrenn/sandbox/internal/hostagent"
|
||||
"git.omukk.dev/wrenn/sandbox/internal/sandbox"
|
||||
"git.omukk.dev/wrenn/sandbox/proto/hostagent/gen/hostagentv1connect"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -39,112 +29,66 @@ func main() {
|
||||
slog.Warn("failed to enable ip_forward", "error", err)
|
||||
}
|
||||
|
||||
listenAddr := envOrDefault("AGENT_LISTEN_ADDR", ":50051")
|
||||
kernelPath := envOrDefault("AGENT_KERNEL_PATH", "/var/lib/wrenn/kernels/vmlinux")
|
||||
imagesPath := envOrDefault("AGENT_IMAGES_PATH", "/var/lib/wrenn/images")
|
||||
sandboxesPath := envOrDefault("AGENT_SANDBOXES_PATH", "/var/lib/wrenn/sandboxes")
|
||||
|
||||
cfg := sandbox.Config{
|
||||
KernelPath: kernelPath,
|
||||
ImagesDir: imagesPath,
|
||||
SandboxesDir: sandboxesPath,
|
||||
}
|
||||
|
||||
mgr := sandbox.New(cfg)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Handle signals for clean shutdown.
|
||||
mgr.StartTTLReaper(ctx)
|
||||
|
||||
srv := hostagent.NewServer(mgr)
|
||||
path, handler := hostagentv1connect.NewHostAgentServiceHandler(srv)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle(path, handler)
|
||||
|
||||
httpServer := &http.Server{
|
||||
Addr: listenAddr,
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
// Graceful shutdown on signal.
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
sig := <-sigCh
|
||||
slog.Info("received signal, shutting down", "signal", sig)
|
||||
cancel()
|
||||
|
||||
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer shutdownCancel()
|
||||
|
||||
mgr.Shutdown(shutdownCtx)
|
||||
|
||||
if err := httpServer.Shutdown(shutdownCtx); err != nil {
|
||||
slog.Error("http server shutdown error", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := run(ctx); err != nil {
|
||||
slog.Error("fatal error", "error", err)
|
||||
slog.Info("host agent starting", "addr", listenAddr)
|
||||
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
slog.Error("http server error", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
slog.Info("host agent stopped")
|
||||
}
|
||||
|
||||
func run(ctx context.Context) error {
|
||||
// Step 1: Clone rootfs for this sandbox.
|
||||
sandboxRootfs := filepath.Join(sandboxesDir, fmt.Sprintf("%s-rootfs.ext4", sandboxID))
|
||||
slog.Info("cloning rootfs", "src", baseRootfs, "dst", sandboxRootfs)
|
||||
|
||||
if err := cloneRootfs(baseRootfs, sandboxRootfs); err != nil {
|
||||
return fmt.Errorf("clone rootfs: %w", err)
|
||||
func envOrDefault(key, def string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
defer os.Remove(sandboxRootfs)
|
||||
|
||||
// Step 2: Set up network.
|
||||
slot := network.NewSlot(slotIndex)
|
||||
|
||||
slog.Info("setting up network", "slot", slotIndex)
|
||||
if err := network.CreateNetwork(slot); err != nil {
|
||||
return fmt.Errorf("create network: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
slog.Info("tearing down network")
|
||||
network.RemoveNetwork(slot)
|
||||
}()
|
||||
|
||||
// Step 3: Boot the VM.
|
||||
mgr := vm.NewManager()
|
||||
|
||||
cfg := vm.VMConfig{
|
||||
SandboxID: sandboxID,
|
||||
KernelPath: kernelPath,
|
||||
RootfsPath: sandboxRootfs,
|
||||
VCPUs: 1,
|
||||
MemoryMB: 512,
|
||||
NetworkNamespace: slot.NamespaceID,
|
||||
TapDevice: slot.TapName,
|
||||
TapMAC: slot.TapMAC,
|
||||
GuestIP: slot.GuestIP,
|
||||
GatewayIP: slot.TapIP,
|
||||
NetMask: slot.GuestNetMask,
|
||||
}
|
||||
|
||||
vmInstance, err := mgr.Create(ctx, cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create VM: %w", err)
|
||||
}
|
||||
_ = vmInstance
|
||||
defer func() {
|
||||
slog.Info("destroying VM")
|
||||
mgr.Destroy(context.Background(), sandboxID)
|
||||
}()
|
||||
|
||||
// Step 4: Wait for envd to be ready.
|
||||
client := envdclient.New(slot.HostIP.String())
|
||||
|
||||
waitCtx, waitCancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer waitCancel()
|
||||
|
||||
if err := client.WaitUntilReady(waitCtx); err != nil {
|
||||
return fmt.Errorf("wait for envd: %w", err)
|
||||
}
|
||||
|
||||
// Step 5: Run "echo hello" inside the sandbox.
|
||||
slog.Info("executing command", "cmd", "echo hello")
|
||||
|
||||
result, err := client.Exec(ctx, "/bin/sh", "-c", "echo hello")
|
||||
if err != nil {
|
||||
return fmt.Errorf("exec: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("\n=== Command Output ===\n")
|
||||
fmt.Printf("stdout: %s", string(result.Stdout))
|
||||
if len(result.Stderr) > 0 {
|
||||
fmt.Printf("stderr: %s", string(result.Stderr))
|
||||
}
|
||||
fmt.Printf("exit code: %d\n", result.ExitCode)
|
||||
fmt.Printf("======================\n\n")
|
||||
|
||||
// Step 6: Clean shutdown.
|
||||
slog.Info("demo complete, cleaning up")
|
||||
|
||||
return nil
|
||||
return def
|
||||
}
|
||||
|
||||
// cloneRootfs creates a copy-on-write clone of the base rootfs image.
|
||||
// Uses reflink if supported by the filesystem, falls back to regular copy.
|
||||
func cloneRootfs(src, dst string) error {
|
||||
// Try reflink first (instant, CoW).
|
||||
cmd := exec.Command("cp", "--reflink=auto", src, dst)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("cp --reflink=auto: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
1444
envd/internal/services/spec/filesystem.pb.go
Normal file
1444
envd/internal/services/spec/filesystem.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
1970
envd/internal/services/spec/process.pb.go
Normal file
1970
envd/internal/services/spec/process.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
337
envd/internal/services/spec/specconnect/filesystem.connect.go
Normal file
337
envd/internal/services/spec/specconnect/filesystem.connect.go
Normal file
@ -0,0 +1,337 @@
|
||||
// Code generated by protoc-gen-connect-go. DO NOT EDIT.
|
||||
//
|
||||
// Source: filesystem.proto
|
||||
|
||||
package specconnect
|
||||
|
||||
import (
|
||||
connect "connectrpc.com/connect"
|
||||
context "context"
|
||||
errors "errors"
|
||||
spec "git.omukk.dev/wrenn/sandbox/envd/internal/services/spec"
|
||||
http "net/http"
|
||||
strings "strings"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file and the connect package are
|
||||
// compatible. If you get a compiler error that this constant is not defined, this code was
|
||||
// generated with a version of connect newer than the one compiled into your binary. You can fix the
|
||||
// problem by either regenerating this code with an older version of connect or updating the connect
|
||||
// version compiled into your binary.
|
||||
const _ = connect.IsAtLeastVersion1_13_0
|
||||
|
||||
const (
|
||||
// FilesystemName is the fully-qualified name of the Filesystem service.
|
||||
FilesystemName = "filesystem.Filesystem"
|
||||
)
|
||||
|
||||
// These constants are the fully-qualified names of the RPCs defined in this package. They're
|
||||
// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.
|
||||
//
|
||||
// Note that these are different from the fully-qualified method names used by
|
||||
// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
|
||||
// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
|
||||
// period.
|
||||
const (
|
||||
// FilesystemStatProcedure is the fully-qualified name of the Filesystem's Stat RPC.
|
||||
FilesystemStatProcedure = "/filesystem.Filesystem/Stat"
|
||||
// FilesystemMakeDirProcedure is the fully-qualified name of the Filesystem's MakeDir RPC.
|
||||
FilesystemMakeDirProcedure = "/filesystem.Filesystem/MakeDir"
|
||||
// FilesystemMoveProcedure is the fully-qualified name of the Filesystem's Move RPC.
|
||||
FilesystemMoveProcedure = "/filesystem.Filesystem/Move"
|
||||
// FilesystemListDirProcedure is the fully-qualified name of the Filesystem's ListDir RPC.
|
||||
FilesystemListDirProcedure = "/filesystem.Filesystem/ListDir"
|
||||
// FilesystemRemoveProcedure is the fully-qualified name of the Filesystem's Remove RPC.
|
||||
FilesystemRemoveProcedure = "/filesystem.Filesystem/Remove"
|
||||
// FilesystemWatchDirProcedure is the fully-qualified name of the Filesystem's WatchDir RPC.
|
||||
FilesystemWatchDirProcedure = "/filesystem.Filesystem/WatchDir"
|
||||
// FilesystemCreateWatcherProcedure is the fully-qualified name of the Filesystem's CreateWatcher
|
||||
// RPC.
|
||||
FilesystemCreateWatcherProcedure = "/filesystem.Filesystem/CreateWatcher"
|
||||
// FilesystemGetWatcherEventsProcedure is the fully-qualified name of the Filesystem's
|
||||
// GetWatcherEvents RPC.
|
||||
FilesystemGetWatcherEventsProcedure = "/filesystem.Filesystem/GetWatcherEvents"
|
||||
// FilesystemRemoveWatcherProcedure is the fully-qualified name of the Filesystem's RemoveWatcher
|
||||
// RPC.
|
||||
FilesystemRemoveWatcherProcedure = "/filesystem.Filesystem/RemoveWatcher"
|
||||
)
|
||||
|
||||
// FilesystemClient is a client for the filesystem.Filesystem service.
|
||||
type FilesystemClient interface {
|
||||
Stat(context.Context, *connect.Request[spec.StatRequest]) (*connect.Response[spec.StatResponse], error)
|
||||
MakeDir(context.Context, *connect.Request[spec.MakeDirRequest]) (*connect.Response[spec.MakeDirResponse], error)
|
||||
Move(context.Context, *connect.Request[spec.MoveRequest]) (*connect.Response[spec.MoveResponse], error)
|
||||
ListDir(context.Context, *connect.Request[spec.ListDirRequest]) (*connect.Response[spec.ListDirResponse], error)
|
||||
Remove(context.Context, *connect.Request[spec.RemoveRequest]) (*connect.Response[spec.RemoveResponse], error)
|
||||
WatchDir(context.Context, *connect.Request[spec.WatchDirRequest]) (*connect.ServerStreamForClient[spec.WatchDirResponse], error)
|
||||
// Non-streaming versions of WatchDir
|
||||
CreateWatcher(context.Context, *connect.Request[spec.CreateWatcherRequest]) (*connect.Response[spec.CreateWatcherResponse], error)
|
||||
GetWatcherEvents(context.Context, *connect.Request[spec.GetWatcherEventsRequest]) (*connect.Response[spec.GetWatcherEventsResponse], error)
|
||||
RemoveWatcher(context.Context, *connect.Request[spec.RemoveWatcherRequest]) (*connect.Response[spec.RemoveWatcherResponse], error)
|
||||
}
|
||||
|
||||
// NewFilesystemClient constructs a client for the filesystem.Filesystem service. By default, it
|
||||
// uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends
|
||||
// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or
|
||||
// connect.WithGRPCWeb() options.
|
||||
//
|
||||
// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
|
||||
// http://api.acme.com or https://acme.com/grpc).
|
||||
func NewFilesystemClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) FilesystemClient {
|
||||
baseURL = strings.TrimRight(baseURL, "/")
|
||||
filesystemMethods := spec.File_filesystem_proto.Services().ByName("Filesystem").Methods()
|
||||
return &filesystemClient{
|
||||
stat: connect.NewClient[spec.StatRequest, spec.StatResponse](
|
||||
httpClient,
|
||||
baseURL+FilesystemStatProcedure,
|
||||
connect.WithSchema(filesystemMethods.ByName("Stat")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
makeDir: connect.NewClient[spec.MakeDirRequest, spec.MakeDirResponse](
|
||||
httpClient,
|
||||
baseURL+FilesystemMakeDirProcedure,
|
||||
connect.WithSchema(filesystemMethods.ByName("MakeDir")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
move: connect.NewClient[spec.MoveRequest, spec.MoveResponse](
|
||||
httpClient,
|
||||
baseURL+FilesystemMoveProcedure,
|
||||
connect.WithSchema(filesystemMethods.ByName("Move")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
listDir: connect.NewClient[spec.ListDirRequest, spec.ListDirResponse](
|
||||
httpClient,
|
||||
baseURL+FilesystemListDirProcedure,
|
||||
connect.WithSchema(filesystemMethods.ByName("ListDir")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
remove: connect.NewClient[spec.RemoveRequest, spec.RemoveResponse](
|
||||
httpClient,
|
||||
baseURL+FilesystemRemoveProcedure,
|
||||
connect.WithSchema(filesystemMethods.ByName("Remove")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
watchDir: connect.NewClient[spec.WatchDirRequest, spec.WatchDirResponse](
|
||||
httpClient,
|
||||
baseURL+FilesystemWatchDirProcedure,
|
||||
connect.WithSchema(filesystemMethods.ByName("WatchDir")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
createWatcher: connect.NewClient[spec.CreateWatcherRequest, spec.CreateWatcherResponse](
|
||||
httpClient,
|
||||
baseURL+FilesystemCreateWatcherProcedure,
|
||||
connect.WithSchema(filesystemMethods.ByName("CreateWatcher")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
getWatcherEvents: connect.NewClient[spec.GetWatcherEventsRequest, spec.GetWatcherEventsResponse](
|
||||
httpClient,
|
||||
baseURL+FilesystemGetWatcherEventsProcedure,
|
||||
connect.WithSchema(filesystemMethods.ByName("GetWatcherEvents")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
removeWatcher: connect.NewClient[spec.RemoveWatcherRequest, spec.RemoveWatcherResponse](
|
||||
httpClient,
|
||||
baseURL+FilesystemRemoveWatcherProcedure,
|
||||
connect.WithSchema(filesystemMethods.ByName("RemoveWatcher")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// filesystemClient implements FilesystemClient.
|
||||
type filesystemClient struct {
|
||||
stat *connect.Client[spec.StatRequest, spec.StatResponse]
|
||||
makeDir *connect.Client[spec.MakeDirRequest, spec.MakeDirResponse]
|
||||
move *connect.Client[spec.MoveRequest, spec.MoveResponse]
|
||||
listDir *connect.Client[spec.ListDirRequest, spec.ListDirResponse]
|
||||
remove *connect.Client[spec.RemoveRequest, spec.RemoveResponse]
|
||||
watchDir *connect.Client[spec.WatchDirRequest, spec.WatchDirResponse]
|
||||
createWatcher *connect.Client[spec.CreateWatcherRequest, spec.CreateWatcherResponse]
|
||||
getWatcherEvents *connect.Client[spec.GetWatcherEventsRequest, spec.GetWatcherEventsResponse]
|
||||
removeWatcher *connect.Client[spec.RemoveWatcherRequest, spec.RemoveWatcherResponse]
|
||||
}
|
||||
|
||||
// Stat calls filesystem.Filesystem.Stat.
|
||||
func (c *filesystemClient) Stat(ctx context.Context, req *connect.Request[spec.StatRequest]) (*connect.Response[spec.StatResponse], error) {
|
||||
return c.stat.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// MakeDir calls filesystem.Filesystem.MakeDir.
|
||||
func (c *filesystemClient) MakeDir(ctx context.Context, req *connect.Request[spec.MakeDirRequest]) (*connect.Response[spec.MakeDirResponse], error) {
|
||||
return c.makeDir.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// Move calls filesystem.Filesystem.Move.
|
||||
func (c *filesystemClient) Move(ctx context.Context, req *connect.Request[spec.MoveRequest]) (*connect.Response[spec.MoveResponse], error) {
|
||||
return c.move.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// ListDir calls filesystem.Filesystem.ListDir.
|
||||
func (c *filesystemClient) ListDir(ctx context.Context, req *connect.Request[spec.ListDirRequest]) (*connect.Response[spec.ListDirResponse], error) {
|
||||
return c.listDir.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// Remove calls filesystem.Filesystem.Remove.
|
||||
func (c *filesystemClient) Remove(ctx context.Context, req *connect.Request[spec.RemoveRequest]) (*connect.Response[spec.RemoveResponse], error) {
|
||||
return c.remove.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// WatchDir calls filesystem.Filesystem.WatchDir.
|
||||
func (c *filesystemClient) WatchDir(ctx context.Context, req *connect.Request[spec.WatchDirRequest]) (*connect.ServerStreamForClient[spec.WatchDirResponse], error) {
|
||||
return c.watchDir.CallServerStream(ctx, req)
|
||||
}
|
||||
|
||||
// CreateWatcher calls filesystem.Filesystem.CreateWatcher.
|
||||
func (c *filesystemClient) CreateWatcher(ctx context.Context, req *connect.Request[spec.CreateWatcherRequest]) (*connect.Response[spec.CreateWatcherResponse], error) {
|
||||
return c.createWatcher.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// GetWatcherEvents calls filesystem.Filesystem.GetWatcherEvents.
|
||||
func (c *filesystemClient) GetWatcherEvents(ctx context.Context, req *connect.Request[spec.GetWatcherEventsRequest]) (*connect.Response[spec.GetWatcherEventsResponse], error) {
|
||||
return c.getWatcherEvents.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// RemoveWatcher calls filesystem.Filesystem.RemoveWatcher.
|
||||
func (c *filesystemClient) RemoveWatcher(ctx context.Context, req *connect.Request[spec.RemoveWatcherRequest]) (*connect.Response[spec.RemoveWatcherResponse], error) {
|
||||
return c.removeWatcher.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// FilesystemHandler is an implementation of the filesystem.Filesystem service.
|
||||
type FilesystemHandler interface {
|
||||
Stat(context.Context, *connect.Request[spec.StatRequest]) (*connect.Response[spec.StatResponse], error)
|
||||
MakeDir(context.Context, *connect.Request[spec.MakeDirRequest]) (*connect.Response[spec.MakeDirResponse], error)
|
||||
Move(context.Context, *connect.Request[spec.MoveRequest]) (*connect.Response[spec.MoveResponse], error)
|
||||
ListDir(context.Context, *connect.Request[spec.ListDirRequest]) (*connect.Response[spec.ListDirResponse], error)
|
||||
Remove(context.Context, *connect.Request[spec.RemoveRequest]) (*connect.Response[spec.RemoveResponse], error)
|
||||
WatchDir(context.Context, *connect.Request[spec.WatchDirRequest], *connect.ServerStream[spec.WatchDirResponse]) error
|
||||
// Non-streaming versions of WatchDir
|
||||
CreateWatcher(context.Context, *connect.Request[spec.CreateWatcherRequest]) (*connect.Response[spec.CreateWatcherResponse], error)
|
||||
GetWatcherEvents(context.Context, *connect.Request[spec.GetWatcherEventsRequest]) (*connect.Response[spec.GetWatcherEventsResponse], error)
|
||||
RemoveWatcher(context.Context, *connect.Request[spec.RemoveWatcherRequest]) (*connect.Response[spec.RemoveWatcherResponse], error)
|
||||
}
|
||||
|
||||
// NewFilesystemHandler builds an HTTP handler from the service implementation. It returns the path
|
||||
// on which to mount the handler and the handler itself.
|
||||
//
|
||||
// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
|
||||
// and JSON codecs. They also support gzip compression.
|
||||
func NewFilesystemHandler(svc FilesystemHandler, opts ...connect.HandlerOption) (string, http.Handler) {
|
||||
filesystemMethods := spec.File_filesystem_proto.Services().ByName("Filesystem").Methods()
|
||||
filesystemStatHandler := connect.NewUnaryHandler(
|
||||
FilesystemStatProcedure,
|
||||
svc.Stat,
|
||||
connect.WithSchema(filesystemMethods.ByName("Stat")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
filesystemMakeDirHandler := connect.NewUnaryHandler(
|
||||
FilesystemMakeDirProcedure,
|
||||
svc.MakeDir,
|
||||
connect.WithSchema(filesystemMethods.ByName("MakeDir")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
filesystemMoveHandler := connect.NewUnaryHandler(
|
||||
FilesystemMoveProcedure,
|
||||
svc.Move,
|
||||
connect.WithSchema(filesystemMethods.ByName("Move")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
filesystemListDirHandler := connect.NewUnaryHandler(
|
||||
FilesystemListDirProcedure,
|
||||
svc.ListDir,
|
||||
connect.WithSchema(filesystemMethods.ByName("ListDir")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
filesystemRemoveHandler := connect.NewUnaryHandler(
|
||||
FilesystemRemoveProcedure,
|
||||
svc.Remove,
|
||||
connect.WithSchema(filesystemMethods.ByName("Remove")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
filesystemWatchDirHandler := connect.NewServerStreamHandler(
|
||||
FilesystemWatchDirProcedure,
|
||||
svc.WatchDir,
|
||||
connect.WithSchema(filesystemMethods.ByName("WatchDir")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
filesystemCreateWatcherHandler := connect.NewUnaryHandler(
|
||||
FilesystemCreateWatcherProcedure,
|
||||
svc.CreateWatcher,
|
||||
connect.WithSchema(filesystemMethods.ByName("CreateWatcher")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
filesystemGetWatcherEventsHandler := connect.NewUnaryHandler(
|
||||
FilesystemGetWatcherEventsProcedure,
|
||||
svc.GetWatcherEvents,
|
||||
connect.WithSchema(filesystemMethods.ByName("GetWatcherEvents")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
filesystemRemoveWatcherHandler := connect.NewUnaryHandler(
|
||||
FilesystemRemoveWatcherProcedure,
|
||||
svc.RemoveWatcher,
|
||||
connect.WithSchema(filesystemMethods.ByName("RemoveWatcher")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
return "/filesystem.Filesystem/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case FilesystemStatProcedure:
|
||||
filesystemStatHandler.ServeHTTP(w, r)
|
||||
case FilesystemMakeDirProcedure:
|
||||
filesystemMakeDirHandler.ServeHTTP(w, r)
|
||||
case FilesystemMoveProcedure:
|
||||
filesystemMoveHandler.ServeHTTP(w, r)
|
||||
case FilesystemListDirProcedure:
|
||||
filesystemListDirHandler.ServeHTTP(w, r)
|
||||
case FilesystemRemoveProcedure:
|
||||
filesystemRemoveHandler.ServeHTTP(w, r)
|
||||
case FilesystemWatchDirProcedure:
|
||||
filesystemWatchDirHandler.ServeHTTP(w, r)
|
||||
case FilesystemCreateWatcherProcedure:
|
||||
filesystemCreateWatcherHandler.ServeHTTP(w, r)
|
||||
case FilesystemGetWatcherEventsProcedure:
|
||||
filesystemGetWatcherEventsHandler.ServeHTTP(w, r)
|
||||
case FilesystemRemoveWatcherProcedure:
|
||||
filesystemRemoveWatcherHandler.ServeHTTP(w, r)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// UnimplementedFilesystemHandler returns CodeUnimplemented from all methods.
|
||||
type UnimplementedFilesystemHandler struct{}
|
||||
|
||||
func (UnimplementedFilesystemHandler) Stat(context.Context, *connect.Request[spec.StatRequest]) (*connect.Response[spec.StatResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("filesystem.Filesystem.Stat is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedFilesystemHandler) MakeDir(context.Context, *connect.Request[spec.MakeDirRequest]) (*connect.Response[spec.MakeDirResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("filesystem.Filesystem.MakeDir is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedFilesystemHandler) Move(context.Context, *connect.Request[spec.MoveRequest]) (*connect.Response[spec.MoveResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("filesystem.Filesystem.Move is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedFilesystemHandler) ListDir(context.Context, *connect.Request[spec.ListDirRequest]) (*connect.Response[spec.ListDirResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("filesystem.Filesystem.ListDir is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedFilesystemHandler) Remove(context.Context, *connect.Request[spec.RemoveRequest]) (*connect.Response[spec.RemoveResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("filesystem.Filesystem.Remove is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedFilesystemHandler) WatchDir(context.Context, *connect.Request[spec.WatchDirRequest], *connect.ServerStream[spec.WatchDirResponse]) error {
|
||||
return connect.NewError(connect.CodeUnimplemented, errors.New("filesystem.Filesystem.WatchDir is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedFilesystemHandler) CreateWatcher(context.Context, *connect.Request[spec.CreateWatcherRequest]) (*connect.Response[spec.CreateWatcherResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("filesystem.Filesystem.CreateWatcher is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedFilesystemHandler) GetWatcherEvents(context.Context, *connect.Request[spec.GetWatcherEventsRequest]) (*connect.Response[spec.GetWatcherEventsResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("filesystem.Filesystem.GetWatcherEvents is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedFilesystemHandler) RemoveWatcher(context.Context, *connect.Request[spec.RemoveWatcherRequest]) (*connect.Response[spec.RemoveWatcherResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("filesystem.Filesystem.RemoveWatcher is not implemented"))
|
||||
}
|
||||
310
envd/internal/services/spec/specconnect/process.connect.go
Normal file
310
envd/internal/services/spec/specconnect/process.connect.go
Normal file
@ -0,0 +1,310 @@
|
||||
// Code generated by protoc-gen-connect-go. DO NOT EDIT.
|
||||
//
|
||||
// Source: process.proto
|
||||
|
||||
package specconnect
|
||||
|
||||
import (
|
||||
connect "connectrpc.com/connect"
|
||||
context "context"
|
||||
errors "errors"
|
||||
spec "git.omukk.dev/wrenn/sandbox/envd/internal/services/spec"
|
||||
http "net/http"
|
||||
strings "strings"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file and the connect package are
|
||||
// compatible. If you get a compiler error that this constant is not defined, this code was
|
||||
// generated with a version of connect newer than the one compiled into your binary. You can fix the
|
||||
// problem by either regenerating this code with an older version of connect or updating the connect
|
||||
// version compiled into your binary.
|
||||
const _ = connect.IsAtLeastVersion1_13_0
|
||||
|
||||
const (
|
||||
// ProcessName is the fully-qualified name of the Process service.
|
||||
ProcessName = "process.Process"
|
||||
)
|
||||
|
||||
// These constants are the fully-qualified names of the RPCs defined in this package. They're
|
||||
// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.
|
||||
//
|
||||
// Note that these are different from the fully-qualified method names used by
|
||||
// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
|
||||
// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
|
||||
// period.
|
||||
const (
|
||||
// ProcessListProcedure is the fully-qualified name of the Process's List RPC.
|
||||
ProcessListProcedure = "/process.Process/List"
|
||||
// ProcessConnectProcedure is the fully-qualified name of the Process's Connect RPC.
|
||||
ProcessConnectProcedure = "/process.Process/Connect"
|
||||
// ProcessStartProcedure is the fully-qualified name of the Process's Start RPC.
|
||||
ProcessStartProcedure = "/process.Process/Start"
|
||||
// ProcessUpdateProcedure is the fully-qualified name of the Process's Update RPC.
|
||||
ProcessUpdateProcedure = "/process.Process/Update"
|
||||
// ProcessStreamInputProcedure is the fully-qualified name of the Process's StreamInput RPC.
|
||||
ProcessStreamInputProcedure = "/process.Process/StreamInput"
|
||||
// ProcessSendInputProcedure is the fully-qualified name of the Process's SendInput RPC.
|
||||
ProcessSendInputProcedure = "/process.Process/SendInput"
|
||||
// ProcessSendSignalProcedure is the fully-qualified name of the Process's SendSignal RPC.
|
||||
ProcessSendSignalProcedure = "/process.Process/SendSignal"
|
||||
// ProcessCloseStdinProcedure is the fully-qualified name of the Process's CloseStdin RPC.
|
||||
ProcessCloseStdinProcedure = "/process.Process/CloseStdin"
|
||||
)
|
||||
|
||||
// ProcessClient is a client for the process.Process service.
|
||||
type ProcessClient interface {
|
||||
List(context.Context, *connect.Request[spec.ListRequest]) (*connect.Response[spec.ListResponse], error)
|
||||
Connect(context.Context, *connect.Request[spec.ConnectRequest]) (*connect.ServerStreamForClient[spec.ConnectResponse], error)
|
||||
Start(context.Context, *connect.Request[spec.StartRequest]) (*connect.ServerStreamForClient[spec.StartResponse], error)
|
||||
Update(context.Context, *connect.Request[spec.UpdateRequest]) (*connect.Response[spec.UpdateResponse], error)
|
||||
// Client input stream ensures ordering of messages
|
||||
StreamInput(context.Context) *connect.ClientStreamForClient[spec.StreamInputRequest, spec.StreamInputResponse]
|
||||
SendInput(context.Context, *connect.Request[spec.SendInputRequest]) (*connect.Response[spec.SendInputResponse], error)
|
||||
SendSignal(context.Context, *connect.Request[spec.SendSignalRequest]) (*connect.Response[spec.SendSignalResponse], error)
|
||||
// Close stdin to signal EOF to the process.
|
||||
// Only works for non-PTY processes. For PTY, send Ctrl+D (0x04) instead.
|
||||
CloseStdin(context.Context, *connect.Request[spec.CloseStdinRequest]) (*connect.Response[spec.CloseStdinResponse], error)
|
||||
}
|
||||
|
||||
// NewProcessClient constructs a client for the process.Process service. By default, it uses the
|
||||
// Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends
|
||||
// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or
|
||||
// connect.WithGRPCWeb() options.
|
||||
//
|
||||
// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
|
||||
// http://api.acme.com or https://acme.com/grpc).
|
||||
func NewProcessClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) ProcessClient {
|
||||
baseURL = strings.TrimRight(baseURL, "/")
|
||||
processMethods := spec.File_process_proto.Services().ByName("Process").Methods()
|
||||
return &processClient{
|
||||
list: connect.NewClient[spec.ListRequest, spec.ListResponse](
|
||||
httpClient,
|
||||
baseURL+ProcessListProcedure,
|
||||
connect.WithSchema(processMethods.ByName("List")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
connect: connect.NewClient[spec.ConnectRequest, spec.ConnectResponse](
|
||||
httpClient,
|
||||
baseURL+ProcessConnectProcedure,
|
||||
connect.WithSchema(processMethods.ByName("Connect")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
start: connect.NewClient[spec.StartRequest, spec.StartResponse](
|
||||
httpClient,
|
||||
baseURL+ProcessStartProcedure,
|
||||
connect.WithSchema(processMethods.ByName("Start")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
update: connect.NewClient[spec.UpdateRequest, spec.UpdateResponse](
|
||||
httpClient,
|
||||
baseURL+ProcessUpdateProcedure,
|
||||
connect.WithSchema(processMethods.ByName("Update")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
streamInput: connect.NewClient[spec.StreamInputRequest, spec.StreamInputResponse](
|
||||
httpClient,
|
||||
baseURL+ProcessStreamInputProcedure,
|
||||
connect.WithSchema(processMethods.ByName("StreamInput")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
sendInput: connect.NewClient[spec.SendInputRequest, spec.SendInputResponse](
|
||||
httpClient,
|
||||
baseURL+ProcessSendInputProcedure,
|
||||
connect.WithSchema(processMethods.ByName("SendInput")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
sendSignal: connect.NewClient[spec.SendSignalRequest, spec.SendSignalResponse](
|
||||
httpClient,
|
||||
baseURL+ProcessSendSignalProcedure,
|
||||
connect.WithSchema(processMethods.ByName("SendSignal")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
closeStdin: connect.NewClient[spec.CloseStdinRequest, spec.CloseStdinResponse](
|
||||
httpClient,
|
||||
baseURL+ProcessCloseStdinProcedure,
|
||||
connect.WithSchema(processMethods.ByName("CloseStdin")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// processClient implements ProcessClient.
|
||||
type processClient struct {
|
||||
list *connect.Client[spec.ListRequest, spec.ListResponse]
|
||||
connect *connect.Client[spec.ConnectRequest, spec.ConnectResponse]
|
||||
start *connect.Client[spec.StartRequest, spec.StartResponse]
|
||||
update *connect.Client[spec.UpdateRequest, spec.UpdateResponse]
|
||||
streamInput *connect.Client[spec.StreamInputRequest, spec.StreamInputResponse]
|
||||
sendInput *connect.Client[spec.SendInputRequest, spec.SendInputResponse]
|
||||
sendSignal *connect.Client[spec.SendSignalRequest, spec.SendSignalResponse]
|
||||
closeStdin *connect.Client[spec.CloseStdinRequest, spec.CloseStdinResponse]
|
||||
}
|
||||
|
||||
// List calls process.Process.List.
|
||||
func (c *processClient) List(ctx context.Context, req *connect.Request[spec.ListRequest]) (*connect.Response[spec.ListResponse], error) {
|
||||
return c.list.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// Connect calls process.Process.Connect.
|
||||
func (c *processClient) Connect(ctx context.Context, req *connect.Request[spec.ConnectRequest]) (*connect.ServerStreamForClient[spec.ConnectResponse], error) {
|
||||
return c.connect.CallServerStream(ctx, req)
|
||||
}
|
||||
|
||||
// Start calls process.Process.Start.
|
||||
func (c *processClient) Start(ctx context.Context, req *connect.Request[spec.StartRequest]) (*connect.ServerStreamForClient[spec.StartResponse], error) {
|
||||
return c.start.CallServerStream(ctx, req)
|
||||
}
|
||||
|
||||
// Update calls process.Process.Update.
|
||||
func (c *processClient) Update(ctx context.Context, req *connect.Request[spec.UpdateRequest]) (*connect.Response[spec.UpdateResponse], error) {
|
||||
return c.update.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// StreamInput calls process.Process.StreamInput.
|
||||
func (c *processClient) StreamInput(ctx context.Context) *connect.ClientStreamForClient[spec.StreamInputRequest, spec.StreamInputResponse] {
|
||||
return c.streamInput.CallClientStream(ctx)
|
||||
}
|
||||
|
||||
// SendInput calls process.Process.SendInput.
|
||||
func (c *processClient) SendInput(ctx context.Context, req *connect.Request[spec.SendInputRequest]) (*connect.Response[spec.SendInputResponse], error) {
|
||||
return c.sendInput.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// SendSignal calls process.Process.SendSignal.
|
||||
func (c *processClient) SendSignal(ctx context.Context, req *connect.Request[spec.SendSignalRequest]) (*connect.Response[spec.SendSignalResponse], error) {
|
||||
return c.sendSignal.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// CloseStdin calls process.Process.CloseStdin.
|
||||
func (c *processClient) CloseStdin(ctx context.Context, req *connect.Request[spec.CloseStdinRequest]) (*connect.Response[spec.CloseStdinResponse], error) {
|
||||
return c.closeStdin.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// ProcessHandler is an implementation of the process.Process service.
|
||||
type ProcessHandler interface {
|
||||
List(context.Context, *connect.Request[spec.ListRequest]) (*connect.Response[spec.ListResponse], error)
|
||||
Connect(context.Context, *connect.Request[spec.ConnectRequest], *connect.ServerStream[spec.ConnectResponse]) error
|
||||
Start(context.Context, *connect.Request[spec.StartRequest], *connect.ServerStream[spec.StartResponse]) error
|
||||
Update(context.Context, *connect.Request[spec.UpdateRequest]) (*connect.Response[spec.UpdateResponse], error)
|
||||
// Client input stream ensures ordering of messages
|
||||
StreamInput(context.Context, *connect.ClientStream[spec.StreamInputRequest]) (*connect.Response[spec.StreamInputResponse], error)
|
||||
SendInput(context.Context, *connect.Request[spec.SendInputRequest]) (*connect.Response[spec.SendInputResponse], error)
|
||||
SendSignal(context.Context, *connect.Request[spec.SendSignalRequest]) (*connect.Response[spec.SendSignalResponse], error)
|
||||
// Close stdin to signal EOF to the process.
|
||||
// Only works for non-PTY processes. For PTY, send Ctrl+D (0x04) instead.
|
||||
CloseStdin(context.Context, *connect.Request[spec.CloseStdinRequest]) (*connect.Response[spec.CloseStdinResponse], error)
|
||||
}
|
||||
|
||||
// NewProcessHandler builds an HTTP handler from the service implementation. It returns the path on
|
||||
// which to mount the handler and the handler itself.
|
||||
//
|
||||
// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
|
||||
// and JSON codecs. They also support gzip compression.
|
||||
func NewProcessHandler(svc ProcessHandler, opts ...connect.HandlerOption) (string, http.Handler) {
|
||||
processMethods := spec.File_process_proto.Services().ByName("Process").Methods()
|
||||
processListHandler := connect.NewUnaryHandler(
|
||||
ProcessListProcedure,
|
||||
svc.List,
|
||||
connect.WithSchema(processMethods.ByName("List")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
processConnectHandler := connect.NewServerStreamHandler(
|
||||
ProcessConnectProcedure,
|
||||
svc.Connect,
|
||||
connect.WithSchema(processMethods.ByName("Connect")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
processStartHandler := connect.NewServerStreamHandler(
|
||||
ProcessStartProcedure,
|
||||
svc.Start,
|
||||
connect.WithSchema(processMethods.ByName("Start")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
processUpdateHandler := connect.NewUnaryHandler(
|
||||
ProcessUpdateProcedure,
|
||||
svc.Update,
|
||||
connect.WithSchema(processMethods.ByName("Update")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
processStreamInputHandler := connect.NewClientStreamHandler(
|
||||
ProcessStreamInputProcedure,
|
||||
svc.StreamInput,
|
||||
connect.WithSchema(processMethods.ByName("StreamInput")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
processSendInputHandler := connect.NewUnaryHandler(
|
||||
ProcessSendInputProcedure,
|
||||
svc.SendInput,
|
||||
connect.WithSchema(processMethods.ByName("SendInput")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
processSendSignalHandler := connect.NewUnaryHandler(
|
||||
ProcessSendSignalProcedure,
|
||||
svc.SendSignal,
|
||||
connect.WithSchema(processMethods.ByName("SendSignal")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
processCloseStdinHandler := connect.NewUnaryHandler(
|
||||
ProcessCloseStdinProcedure,
|
||||
svc.CloseStdin,
|
||||
connect.WithSchema(processMethods.ByName("CloseStdin")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
return "/process.Process/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case ProcessListProcedure:
|
||||
processListHandler.ServeHTTP(w, r)
|
||||
case ProcessConnectProcedure:
|
||||
processConnectHandler.ServeHTTP(w, r)
|
||||
case ProcessStartProcedure:
|
||||
processStartHandler.ServeHTTP(w, r)
|
||||
case ProcessUpdateProcedure:
|
||||
processUpdateHandler.ServeHTTP(w, r)
|
||||
case ProcessStreamInputProcedure:
|
||||
processStreamInputHandler.ServeHTTP(w, r)
|
||||
case ProcessSendInputProcedure:
|
||||
processSendInputHandler.ServeHTTP(w, r)
|
||||
case ProcessSendSignalProcedure:
|
||||
processSendSignalHandler.ServeHTTP(w, r)
|
||||
case ProcessCloseStdinProcedure:
|
||||
processCloseStdinHandler.ServeHTTP(w, r)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// UnimplementedProcessHandler returns CodeUnimplemented from all methods.
|
||||
type UnimplementedProcessHandler struct{}
|
||||
|
||||
func (UnimplementedProcessHandler) List(context.Context, *connect.Request[spec.ListRequest]) (*connect.Response[spec.ListResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("process.Process.List is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedProcessHandler) Connect(context.Context, *connect.Request[spec.ConnectRequest], *connect.ServerStream[spec.ConnectResponse]) error {
|
||||
return connect.NewError(connect.CodeUnimplemented, errors.New("process.Process.Connect is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedProcessHandler) Start(context.Context, *connect.Request[spec.StartRequest], *connect.ServerStream[spec.StartResponse]) error {
|
||||
return connect.NewError(connect.CodeUnimplemented, errors.New("process.Process.Start is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedProcessHandler) Update(context.Context, *connect.Request[spec.UpdateRequest]) (*connect.Response[spec.UpdateResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("process.Process.Update is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedProcessHandler) StreamInput(context.Context, *connect.ClientStream[spec.StreamInputRequest]) (*connect.Response[spec.StreamInputResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("process.Process.StreamInput is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedProcessHandler) SendInput(context.Context, *connect.Request[spec.SendInputRequest]) (*connect.Response[spec.SendInputResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("process.Process.SendInput is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedProcessHandler) SendSignal(context.Context, *connect.Request[spec.SendSignalRequest]) (*connect.Response[spec.SendSignalResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("process.Process.SendSignal is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedProcessHandler) CloseStdin(context.Context, *connect.Request[spec.CloseStdinRequest]) (*connect.Response[spec.CloseStdinResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("process.Process.CloseStdin is not implemented"))
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
[Unit]
|
||||
Description=Wrenn envd guest agent
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/envd
|
||||
Restart=on-failure
|
||||
RestartSec=1
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@ -0,0 +1,16 @@
|
||||
package filesystem
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// CloneRootfs creates a copy-on-write clone of the base rootfs image.
|
||||
// Uses reflink if supported by the filesystem, falls back to regular copy.
|
||||
func CloneRootfs(src, dst string) error {
|
||||
cmd := exec.Command("cp", "--reflink=auto", src, dst)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("cp --reflink=auto: %s: %w", string(out), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -0,0 +1 @@
|
||||
package filesystem
|
||||
|
||||
125
internal/hostagent/server.go
Normal file
125
internal/hostagent/server.go
Normal file
@ -0,0 +1,125 @@
|
||||
package hostagent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
|
||||
pb "git.omukk.dev/wrenn/sandbox/proto/hostagent/gen"
|
||||
"git.omukk.dev/wrenn/sandbox/proto/hostagent/gen/hostagentv1connect"
|
||||
|
||||
"git.omukk.dev/wrenn/sandbox/internal/sandbox"
|
||||
)
|
||||
|
||||
// Server implements the HostAgentService Connect RPC handler.
|
||||
type Server struct {
|
||||
hostagentv1connect.UnimplementedHostAgentServiceHandler
|
||||
mgr *sandbox.Manager
|
||||
}
|
||||
|
||||
// NewServer creates a new host agent RPC server.
|
||||
func NewServer(mgr *sandbox.Manager) *Server {
|
||||
return &Server{mgr: mgr}
|
||||
}
|
||||
|
||||
func (s *Server) CreateSandbox(
|
||||
ctx context.Context,
|
||||
req *connect.Request[pb.CreateSandboxRequest],
|
||||
) (*connect.Response[pb.CreateSandboxResponse], error) {
|
||||
msg := req.Msg
|
||||
|
||||
sb, err := s.mgr.Create(ctx, msg.Template, int(msg.Vcpus), int(msg.MemoryMb), int(msg.TimeoutSec))
|
||||
if err != nil {
|
||||
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("create sandbox: %w", err))
|
||||
}
|
||||
|
||||
return connect.NewResponse(&pb.CreateSandboxResponse{
|
||||
SandboxId: sb.ID,
|
||||
Status: string(sb.Status),
|
||||
HostIp: sb.HostIP.String(),
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (s *Server) DestroySandbox(
|
||||
ctx context.Context,
|
||||
req *connect.Request[pb.DestroySandboxRequest],
|
||||
) (*connect.Response[pb.DestroySandboxResponse], error) {
|
||||
if err := s.mgr.Destroy(ctx, req.Msg.SandboxId); err != nil {
|
||||
return nil, connect.NewError(connect.CodeNotFound, err)
|
||||
}
|
||||
return connect.NewResponse(&pb.DestroySandboxResponse{}), nil
|
||||
}
|
||||
|
||||
func (s *Server) PauseSandbox(
|
||||
ctx context.Context,
|
||||
req *connect.Request[pb.PauseSandboxRequest],
|
||||
) (*connect.Response[pb.PauseSandboxResponse], error) {
|
||||
if err := s.mgr.Pause(ctx, req.Msg.SandboxId); err != nil {
|
||||
return nil, connect.NewError(connect.CodeInternal, err)
|
||||
}
|
||||
return connect.NewResponse(&pb.PauseSandboxResponse{}), nil
|
||||
}
|
||||
|
||||
func (s *Server) ResumeSandbox(
|
||||
ctx context.Context,
|
||||
req *connect.Request[pb.ResumeSandboxRequest],
|
||||
) (*connect.Response[pb.ResumeSandboxResponse], error) {
|
||||
if err := s.mgr.Resume(ctx, req.Msg.SandboxId); err != nil {
|
||||
return nil, connect.NewError(connect.CodeInternal, err)
|
||||
}
|
||||
return connect.NewResponse(&pb.ResumeSandboxResponse{}), nil
|
||||
}
|
||||
|
||||
func (s *Server) Exec(
|
||||
ctx context.Context,
|
||||
req *connect.Request[pb.ExecRequest],
|
||||
) (*connect.Response[pb.ExecResponse], error) {
|
||||
msg := req.Msg
|
||||
|
||||
timeout := 30 * time.Second
|
||||
if msg.TimeoutSec > 0 {
|
||||
timeout = time.Duration(msg.TimeoutSec) * time.Second
|
||||
}
|
||||
|
||||
execCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
result, err := s.mgr.Exec(execCtx, msg.SandboxId, msg.Cmd, msg.Args...)
|
||||
if err != nil {
|
||||
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("exec: %w", err))
|
||||
}
|
||||
|
||||
return connect.NewResponse(&pb.ExecResponse{
|
||||
Stdout: result.Stdout,
|
||||
Stderr: result.Stderr,
|
||||
ExitCode: result.ExitCode,
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (s *Server) ListSandboxes(
|
||||
ctx context.Context,
|
||||
req *connect.Request[pb.ListSandboxesRequest],
|
||||
) (*connect.Response[pb.ListSandboxesResponse], error) {
|
||||
sandboxes := s.mgr.List()
|
||||
|
||||
infos := make([]*pb.SandboxInfo, len(sandboxes))
|
||||
for i, sb := range sandboxes {
|
||||
infos[i] = &pb.SandboxInfo{
|
||||
SandboxId: sb.ID,
|
||||
Status: string(sb.Status),
|
||||
Template: sb.Template,
|
||||
Vcpus: int32(sb.VCPUs),
|
||||
MemoryMb: int32(sb.MemoryMB),
|
||||
HostIp: sb.HostIP.String(),
|
||||
CreatedAtUnix: sb.CreatedAt.Unix(),
|
||||
LastActiveAtUnix: sb.LastActiveAt.Unix(),
|
||||
TimeoutSec: int32(sb.TimeoutSec),
|
||||
}
|
||||
}
|
||||
|
||||
return connect.NewResponse(&pb.ListSandboxesResponse{
|
||||
Sandboxes: infos,
|
||||
}), nil
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package id
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// NewSandboxID generates a new sandbox ID in the format "sb-" + 8 hex chars.
|
||||
func NewSandboxID() string {
|
||||
b := make([]byte, 4)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
panic(fmt.Sprintf("crypto/rand failed: %v", err))
|
||||
}
|
||||
return "sb-" + hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
@ -0,0 +1 @@
|
||||
package models
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SandboxStatus represents the current state of a sandbox.
|
||||
type SandboxStatus string
|
||||
|
||||
const (
|
||||
StatusPending SandboxStatus = "pending"
|
||||
StatusRunning SandboxStatus = "running"
|
||||
StatusPaused SandboxStatus = "paused"
|
||||
StatusStopped SandboxStatus = "stopped"
|
||||
StatusError SandboxStatus = "error"
|
||||
)
|
||||
|
||||
// Sandbox holds all state for a running sandbox on this host.
|
||||
type Sandbox struct {
|
||||
ID string
|
||||
Status SandboxStatus
|
||||
Template string
|
||||
VCPUs int
|
||||
MemoryMB int
|
||||
TimeoutSec int
|
||||
SlotIndex int
|
||||
HostIP net.IP
|
||||
RootfsPath string
|
||||
CreatedAt time.Time
|
||||
LastActiveAt time.Time
|
||||
}
|
||||
|
||||
@ -1 +1,41 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// SlotAllocator manages network slot indices for sandboxes.
|
||||
// Each sandbox needs a unique slot index for its network addressing.
|
||||
type SlotAllocator struct {
|
||||
mu sync.Mutex
|
||||
inUse map[int]bool
|
||||
}
|
||||
|
||||
// NewSlotAllocator creates a new slot allocator.
|
||||
func NewSlotAllocator() *SlotAllocator {
|
||||
return &SlotAllocator{
|
||||
inUse: make(map[int]bool),
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate returns the next available slot index (1-based).
|
||||
func (a *SlotAllocator) Allocate() (int, error) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
for i := 1; i <= 65534; i++ {
|
||||
if !a.inUse[i] {
|
||||
a.inUse[i] = true
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("no free network slots")
|
||||
}
|
||||
|
||||
// Release frees a slot index for reuse.
|
||||
func (a *SlotAllocator) Release(index int) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
delete(a.inUse, index)
|
||||
}
|
||||
|
||||
356
internal/sandbox/manager.go
Normal file
356
internal/sandbox/manager.go
Normal file
@ -0,0 +1,356 @@
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.omukk.dev/wrenn/sandbox/internal/envdclient"
|
||||
"git.omukk.dev/wrenn/sandbox/internal/filesystem"
|
||||
"git.omukk.dev/wrenn/sandbox/internal/id"
|
||||
"git.omukk.dev/wrenn/sandbox/internal/models"
|
||||
"git.omukk.dev/wrenn/sandbox/internal/network"
|
||||
"git.omukk.dev/wrenn/sandbox/internal/vm"
|
||||
)
|
||||
|
||||
// Config holds the paths and defaults for the sandbox manager.
|
||||
type Config struct {
|
||||
KernelPath string
|
||||
ImagesDir string // directory containing base rootfs images (e.g., /var/lib/wrenn/images/minimal.ext4)
|
||||
SandboxesDir string // directory for per-sandbox rootfs clones (e.g., /var/lib/wrenn/sandboxes)
|
||||
EnvdTimeout time.Duration
|
||||
}
|
||||
|
||||
// Manager orchestrates sandbox lifecycle: VM, network, filesystem, envd.
|
||||
type Manager struct {
|
||||
cfg Config
|
||||
vm *vm.Manager
|
||||
slots *network.SlotAllocator
|
||||
mu sync.RWMutex
|
||||
boxes map[string]*sandboxState
|
||||
stopCh chan struct{}
|
||||
}
|
||||
|
||||
// sandboxState holds the runtime state for a single sandbox.
|
||||
type sandboxState struct {
|
||||
models.Sandbox
|
||||
slot *network.Slot
|
||||
client *envdclient.Client
|
||||
}
|
||||
|
||||
// New creates a new sandbox manager.
|
||||
func New(cfg Config) *Manager {
|
||||
if cfg.EnvdTimeout == 0 {
|
||||
cfg.EnvdTimeout = 30 * time.Second
|
||||
}
|
||||
return &Manager{
|
||||
cfg: cfg,
|
||||
vm: vm.NewManager(),
|
||||
slots: network.NewSlotAllocator(),
|
||||
boxes: make(map[string]*sandboxState),
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Create boots a new sandbox: clone rootfs, set up network, start VM, wait for envd.
|
||||
func (m *Manager) Create(ctx context.Context, template string, vcpus, memoryMB, timeoutSec int) (*models.Sandbox, error) {
|
||||
sandboxID := id.NewSandboxID()
|
||||
|
||||
if vcpus <= 0 {
|
||||
vcpus = 1
|
||||
}
|
||||
if memoryMB <= 0 {
|
||||
memoryMB = 512
|
||||
}
|
||||
|
||||
if template == "" {
|
||||
template = "minimal"
|
||||
}
|
||||
|
||||
// Resolve base rootfs image: /var/lib/wrenn/images/{template}.ext4
|
||||
baseRootfs := filepath.Join(m.cfg.ImagesDir, template+".ext4")
|
||||
if _, err := os.Stat(baseRootfs); err != nil {
|
||||
return nil, fmt.Errorf("base rootfs not found at %s: %w", baseRootfs, err)
|
||||
}
|
||||
|
||||
// Clone rootfs.
|
||||
rootfsPath := filepath.Join(m.cfg.SandboxesDir, fmt.Sprintf("%s-%s.ext4", sandboxID, template))
|
||||
if err := filesystem.CloneRootfs(baseRootfs, rootfsPath); err != nil {
|
||||
return nil, fmt.Errorf("clone rootfs: %w", err)
|
||||
}
|
||||
|
||||
// Allocate network slot.
|
||||
slotIdx, err := m.slots.Allocate()
|
||||
if err != nil {
|
||||
os.Remove(rootfsPath)
|
||||
return nil, fmt.Errorf("allocate network slot: %w", err)
|
||||
}
|
||||
slot := network.NewSlot(slotIdx)
|
||||
|
||||
// Set up network.
|
||||
if err := network.CreateNetwork(slot); err != nil {
|
||||
m.slots.Release(slotIdx)
|
||||
os.Remove(rootfsPath)
|
||||
return nil, fmt.Errorf("create network: %w", err)
|
||||
}
|
||||
|
||||
// Boot VM.
|
||||
vmCfg := vm.VMConfig{
|
||||
SandboxID: sandboxID,
|
||||
KernelPath: m.cfg.KernelPath,
|
||||
RootfsPath: rootfsPath,
|
||||
VCPUs: vcpus,
|
||||
MemoryMB: memoryMB,
|
||||
NetworkNamespace: slot.NamespaceID,
|
||||
TapDevice: slot.TapName,
|
||||
TapMAC: slot.TapMAC,
|
||||
GuestIP: slot.GuestIP,
|
||||
GatewayIP: slot.TapIP,
|
||||
NetMask: slot.GuestNetMask,
|
||||
}
|
||||
|
||||
if _, err := m.vm.Create(ctx, vmCfg); err != nil {
|
||||
network.RemoveNetwork(slot)
|
||||
m.slots.Release(slotIdx)
|
||||
os.Remove(rootfsPath)
|
||||
return nil, fmt.Errorf("create VM: %w", err)
|
||||
}
|
||||
|
||||
// Wait for envd to be ready.
|
||||
client := envdclient.New(slot.HostIP.String())
|
||||
waitCtx, waitCancel := context.WithTimeout(ctx, m.cfg.EnvdTimeout)
|
||||
defer waitCancel()
|
||||
|
||||
if err := client.WaitUntilReady(waitCtx); err != nil {
|
||||
m.vm.Destroy(context.Background(), sandboxID)
|
||||
network.RemoveNetwork(slot)
|
||||
m.slots.Release(slotIdx)
|
||||
os.Remove(rootfsPath)
|
||||
return nil, fmt.Errorf("wait for envd: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
sb := &sandboxState{
|
||||
Sandbox: models.Sandbox{
|
||||
ID: sandboxID,
|
||||
Status: models.StatusRunning,
|
||||
Template: template,
|
||||
VCPUs: vcpus,
|
||||
MemoryMB: memoryMB,
|
||||
TimeoutSec: timeoutSec,
|
||||
SlotIndex: slotIdx,
|
||||
HostIP: slot.HostIP,
|
||||
RootfsPath: rootfsPath,
|
||||
CreatedAt: now,
|
||||
LastActiveAt: now,
|
||||
},
|
||||
slot: slot,
|
||||
client: client,
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
m.boxes[sandboxID] = sb
|
||||
m.mu.Unlock()
|
||||
|
||||
slog.Info("sandbox created",
|
||||
"id", sandboxID,
|
||||
"template", template,
|
||||
"host_ip", slot.HostIP.String(),
|
||||
)
|
||||
|
||||
return &sb.Sandbox, nil
|
||||
}
|
||||
|
||||
// Destroy stops and cleans up a sandbox.
|
||||
func (m *Manager) Destroy(ctx context.Context, sandboxID string) error {
|
||||
m.mu.Lock()
|
||||
sb, ok := m.boxes[sandboxID]
|
||||
if !ok {
|
||||
m.mu.Unlock()
|
||||
return fmt.Errorf("sandbox not found: %s", sandboxID)
|
||||
}
|
||||
delete(m.boxes, sandboxID)
|
||||
m.mu.Unlock()
|
||||
|
||||
m.cleanup(ctx, sb)
|
||||
|
||||
slog.Info("sandbox destroyed", "id", sandboxID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// cleanup tears down all resources for a sandbox.
|
||||
func (m *Manager) cleanup(ctx context.Context, sb *sandboxState) {
|
||||
if err := m.vm.Destroy(ctx, sb.ID); err != nil {
|
||||
slog.Warn("vm destroy error", "id", sb.ID, "error", err)
|
||||
}
|
||||
if err := network.RemoveNetwork(sb.slot); err != nil {
|
||||
slog.Warn("network cleanup error", "id", sb.ID, "error", err)
|
||||
}
|
||||
m.slots.Release(sb.SlotIndex)
|
||||
os.Remove(sb.RootfsPath)
|
||||
}
|
||||
|
||||
// Pause pauses a running sandbox.
|
||||
func (m *Manager) Pause(ctx context.Context, sandboxID string) error {
|
||||
sb, err := m.get(sandboxID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if sb.Status != models.StatusRunning {
|
||||
return fmt.Errorf("sandbox %s is not running (status: %s)", sandboxID, sb.Status)
|
||||
}
|
||||
|
||||
if err := m.vm.Pause(ctx, sandboxID); err != nil {
|
||||
return fmt.Errorf("pause VM: %w", err)
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
sb.Status = models.StatusPaused
|
||||
m.mu.Unlock()
|
||||
|
||||
slog.Info("sandbox paused", "id", sandboxID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resume resumes a paused sandbox.
|
||||
func (m *Manager) Resume(ctx context.Context, sandboxID string) error {
|
||||
sb, err := m.get(sandboxID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if sb.Status != models.StatusPaused {
|
||||
return fmt.Errorf("sandbox %s is not paused (status: %s)", sandboxID, sb.Status)
|
||||
}
|
||||
|
||||
if err := m.vm.Resume(ctx, sandboxID); err != nil {
|
||||
return fmt.Errorf("resume VM: %w", err)
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
sb.Status = models.StatusRunning
|
||||
sb.LastActiveAt = time.Now()
|
||||
m.mu.Unlock()
|
||||
|
||||
slog.Info("sandbox resumed", "id", sandboxID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Exec runs a command inside a sandbox.
|
||||
func (m *Manager) Exec(ctx context.Context, sandboxID string, cmd string, args ...string) (*envdclient.ExecResult, error) {
|
||||
sb, err := m.get(sandboxID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if sb.Status != models.StatusRunning {
|
||||
return nil, fmt.Errorf("sandbox %s is not running (status: %s)", sandboxID, sb.Status)
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
sb.LastActiveAt = time.Now()
|
||||
m.mu.Unlock()
|
||||
|
||||
return sb.client.Exec(ctx, cmd, args...)
|
||||
}
|
||||
|
||||
// List returns all sandboxes.
|
||||
func (m *Manager) List() []models.Sandbox {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
result := make([]models.Sandbox, 0, len(m.boxes))
|
||||
for _, sb := range m.boxes {
|
||||
result = append(result, sb.Sandbox)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Get returns a sandbox by ID.
|
||||
func (m *Manager) Get(sandboxID string) (*models.Sandbox, error) {
|
||||
sb, err := m.get(sandboxID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sb.Sandbox, nil
|
||||
}
|
||||
|
||||
func (m *Manager) get(sandboxID string) (*sandboxState, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
sb, ok := m.boxes[sandboxID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("sandbox not found: %s", sandboxID)
|
||||
}
|
||||
return sb, nil
|
||||
}
|
||||
|
||||
// StartTTLReaper starts a background goroutine that destroys sandboxes
|
||||
// that have exceeded their TTL (timeout_sec of inactivity).
|
||||
func (m *Manager) StartTTLReaper(ctx context.Context) {
|
||||
go func() {
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-m.stopCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
m.reapExpired(ctx)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (m *Manager) reapExpired(ctx context.Context) {
|
||||
m.mu.RLock()
|
||||
var expired []string
|
||||
now := time.Now()
|
||||
for id, sb := range m.boxes {
|
||||
if sb.TimeoutSec <= 0 {
|
||||
continue
|
||||
}
|
||||
if sb.Status != models.StatusRunning && sb.Status != models.StatusPaused {
|
||||
continue
|
||||
}
|
||||
if now.Sub(sb.LastActiveAt) > time.Duration(sb.TimeoutSec)*time.Second {
|
||||
expired = append(expired, id)
|
||||
}
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
|
||||
for _, id := range expired {
|
||||
slog.Info("TTL expired, destroying sandbox", "id", id)
|
||||
if err := m.Destroy(ctx, id); err != nil {
|
||||
slog.Warn("TTL reap failed", "id", id, "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown destroys all sandboxes and stops the TTL reaper.
|
||||
func (m *Manager) Shutdown(ctx context.Context) {
|
||||
close(m.stopCh)
|
||||
|
||||
m.mu.Lock()
|
||||
ids := make([]string, 0, len(m.boxes))
|
||||
for id := range m.boxes {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
for _, sbID := range ids {
|
||||
slog.Info("shutdown: destroying sandbox", "id", sbID)
|
||||
if err := m.Destroy(ctx, sbID); err != nil {
|
||||
slog.Warn("shutdown destroy failed", "id", sbID, "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -30,7 +30,10 @@ type process struct {
|
||||
// 5. ip netns exec <ns>: enters the network namespace where TAP is configured
|
||||
// 6. exec firecracker with the API socket path
|
||||
func startProcess(ctx context.Context, cfg *VMConfig) (*process, error) {
|
||||
execCtx, cancel := context.WithCancel(ctx)
|
||||
// Use a background context for the long-lived Firecracker process.
|
||||
// The request context (ctx) is only used for the startup phase — we must
|
||||
// not tie the VM's lifetime to the HTTP request that created it.
|
||||
execCtx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
script := buildStartScript(cfg)
|
||||
|
||||
|
||||
13
proto/hostagent/buf.gen.yaml
Normal file
13
proto/hostagent/buf.gen.yaml
Normal file
@ -0,0 +1,13 @@
|
||||
version: v2
|
||||
plugins:
|
||||
- protoc_builtin: go
|
||||
out: gen
|
||||
opt: paths=source_relative
|
||||
- local: protoc-gen-connect-go
|
||||
out: gen
|
||||
opt: paths=source_relative
|
||||
managed:
|
||||
enabled: true
|
||||
override:
|
||||
- file_option: go_package_prefix
|
||||
value: git.omukk.dev/wrenn/sandbox/proto/hostagent/gen
|
||||
3
proto/hostagent/buf.yaml
Normal file
3
proto/hostagent/buf.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
version: v2
|
||||
modules:
|
||||
- path: .
|
||||
848
proto/hostagent/gen/hostagent.pb.go
Normal file
848
proto/hostagent/gen/hostagent.pb.go
Normal file
@ -0,0 +1,848 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc (unknown)
|
||||
// source: hostagent.proto
|
||||
|
||||
package hostagentv1
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type CreateSandboxRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Template name (e.g., "minimal", "python311"). Determines base rootfs.
|
||||
Template string `protobuf:"bytes,1,opt,name=template,proto3" json:"template,omitempty"`
|
||||
// Number of virtual CPUs (default: 1).
|
||||
Vcpus int32 `protobuf:"varint,2,opt,name=vcpus,proto3" json:"vcpus,omitempty"`
|
||||
// Memory in MB (default: 512).
|
||||
MemoryMb int32 `protobuf:"varint,3,opt,name=memory_mb,json=memoryMb,proto3" json:"memory_mb,omitempty"`
|
||||
// TTL in seconds. Sandbox is auto-destroyed after this duration of
|
||||
// inactivity. 0 means no auto-destroy.
|
||||
TimeoutSec int32 `protobuf:"varint,4,opt,name=timeout_sec,json=timeoutSec,proto3" json:"timeout_sec,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *CreateSandboxRequest) Reset() {
|
||||
*x = CreateSandboxRequest{}
|
||||
mi := &file_hostagent_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CreateSandboxRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CreateSandboxRequest) ProtoMessage() {}
|
||||
|
||||
func (x *CreateSandboxRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_hostagent_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CreateSandboxRequest.ProtoReflect.Descriptor instead.
|
||||
func (*CreateSandboxRequest) Descriptor() ([]byte, []int) {
|
||||
return file_hostagent_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *CreateSandboxRequest) GetTemplate() string {
|
||||
if x != nil {
|
||||
return x.Template
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CreateSandboxRequest) GetVcpus() int32 {
|
||||
if x != nil {
|
||||
return x.Vcpus
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *CreateSandboxRequest) GetMemoryMb() int32 {
|
||||
if x != nil {
|
||||
return x.MemoryMb
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *CreateSandboxRequest) GetTimeoutSec() int32 {
|
||||
if x != nil {
|
||||
return x.TimeoutSec
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type CreateSandboxResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"`
|
||||
Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"`
|
||||
HostIp string `protobuf:"bytes,3,opt,name=host_ip,json=hostIp,proto3" json:"host_ip,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *CreateSandboxResponse) Reset() {
|
||||
*x = CreateSandboxResponse{}
|
||||
mi := &file_hostagent_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CreateSandboxResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CreateSandboxResponse) ProtoMessage() {}
|
||||
|
||||
func (x *CreateSandboxResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_hostagent_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CreateSandboxResponse.ProtoReflect.Descriptor instead.
|
||||
func (*CreateSandboxResponse) Descriptor() ([]byte, []int) {
|
||||
return file_hostagent_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *CreateSandboxResponse) GetSandboxId() string {
|
||||
if x != nil {
|
||||
return x.SandboxId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CreateSandboxResponse) GetStatus() string {
|
||||
if x != nil {
|
||||
return x.Status
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CreateSandboxResponse) GetHostIp() string {
|
||||
if x != nil {
|
||||
return x.HostIp
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type DestroySandboxRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DestroySandboxRequest) Reset() {
|
||||
*x = DestroySandboxRequest{}
|
||||
mi := &file_hostagent_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DestroySandboxRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DestroySandboxRequest) ProtoMessage() {}
|
||||
|
||||
func (x *DestroySandboxRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_hostagent_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DestroySandboxRequest.ProtoReflect.Descriptor instead.
|
||||
func (*DestroySandboxRequest) Descriptor() ([]byte, []int) {
|
||||
return file_hostagent_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *DestroySandboxRequest) GetSandboxId() string {
|
||||
if x != nil {
|
||||
return x.SandboxId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type DestroySandboxResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DestroySandboxResponse) Reset() {
|
||||
*x = DestroySandboxResponse{}
|
||||
mi := &file_hostagent_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DestroySandboxResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DestroySandboxResponse) ProtoMessage() {}
|
||||
|
||||
func (x *DestroySandboxResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_hostagent_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DestroySandboxResponse.ProtoReflect.Descriptor instead.
|
||||
func (*DestroySandboxResponse) Descriptor() ([]byte, []int) {
|
||||
return file_hostagent_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
type PauseSandboxRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *PauseSandboxRequest) Reset() {
|
||||
*x = PauseSandboxRequest{}
|
||||
mi := &file_hostagent_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *PauseSandboxRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*PauseSandboxRequest) ProtoMessage() {}
|
||||
|
||||
func (x *PauseSandboxRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_hostagent_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use PauseSandboxRequest.ProtoReflect.Descriptor instead.
|
||||
func (*PauseSandboxRequest) Descriptor() ([]byte, []int) {
|
||||
return file_hostagent_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *PauseSandboxRequest) GetSandboxId() string {
|
||||
if x != nil {
|
||||
return x.SandboxId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type PauseSandboxResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *PauseSandboxResponse) Reset() {
|
||||
*x = PauseSandboxResponse{}
|
||||
mi := &file_hostagent_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *PauseSandboxResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*PauseSandboxResponse) ProtoMessage() {}
|
||||
|
||||
func (x *PauseSandboxResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_hostagent_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use PauseSandboxResponse.ProtoReflect.Descriptor instead.
|
||||
func (*PauseSandboxResponse) Descriptor() ([]byte, []int) {
|
||||
return file_hostagent_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
type ResumeSandboxRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ResumeSandboxRequest) Reset() {
|
||||
*x = ResumeSandboxRequest{}
|
||||
mi := &file_hostagent_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ResumeSandboxRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ResumeSandboxRequest) ProtoMessage() {}
|
||||
|
||||
func (x *ResumeSandboxRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_hostagent_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ResumeSandboxRequest.ProtoReflect.Descriptor instead.
|
||||
func (*ResumeSandboxRequest) Descriptor() ([]byte, []int) {
|
||||
return file_hostagent_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
func (x *ResumeSandboxRequest) GetSandboxId() string {
|
||||
if x != nil {
|
||||
return x.SandboxId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type ResumeSandboxResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ResumeSandboxResponse) Reset() {
|
||||
*x = ResumeSandboxResponse{}
|
||||
mi := &file_hostagent_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ResumeSandboxResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ResumeSandboxResponse) ProtoMessage() {}
|
||||
|
||||
func (x *ResumeSandboxResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_hostagent_proto_msgTypes[7]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ResumeSandboxResponse.ProtoReflect.Descriptor instead.
|
||||
func (*ResumeSandboxResponse) Descriptor() ([]byte, []int) {
|
||||
return file_hostagent_proto_rawDescGZIP(), []int{7}
|
||||
}
|
||||
|
||||
type ExecRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"`
|
||||
Cmd string `protobuf:"bytes,2,opt,name=cmd,proto3" json:"cmd,omitempty"`
|
||||
Args []string `protobuf:"bytes,3,rep,name=args,proto3" json:"args,omitempty"`
|
||||
// Timeout for the command in seconds (default: 30).
|
||||
TimeoutSec int32 `protobuf:"varint,4,opt,name=timeout_sec,json=timeoutSec,proto3" json:"timeout_sec,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ExecRequest) Reset() {
|
||||
*x = ExecRequest{}
|
||||
mi := &file_hostagent_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ExecRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ExecRequest) ProtoMessage() {}
|
||||
|
||||
func (x *ExecRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_hostagent_proto_msgTypes[8]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ExecRequest.ProtoReflect.Descriptor instead.
|
||||
func (*ExecRequest) Descriptor() ([]byte, []int) {
|
||||
return file_hostagent_proto_rawDescGZIP(), []int{8}
|
||||
}
|
||||
|
||||
func (x *ExecRequest) GetSandboxId() string {
|
||||
if x != nil {
|
||||
return x.SandboxId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ExecRequest) GetCmd() string {
|
||||
if x != nil {
|
||||
return x.Cmd
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ExecRequest) GetArgs() []string {
|
||||
if x != nil {
|
||||
return x.Args
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ExecRequest) GetTimeoutSec() int32 {
|
||||
if x != nil {
|
||||
return x.TimeoutSec
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type ExecResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Stdout []byte `protobuf:"bytes,1,opt,name=stdout,proto3" json:"stdout,omitempty"`
|
||||
Stderr []byte `protobuf:"bytes,2,opt,name=stderr,proto3" json:"stderr,omitempty"`
|
||||
ExitCode int32 `protobuf:"varint,3,opt,name=exit_code,json=exitCode,proto3" json:"exit_code,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ExecResponse) Reset() {
|
||||
*x = ExecResponse{}
|
||||
mi := &file_hostagent_proto_msgTypes[9]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ExecResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ExecResponse) ProtoMessage() {}
|
||||
|
||||
func (x *ExecResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_hostagent_proto_msgTypes[9]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ExecResponse.ProtoReflect.Descriptor instead.
|
||||
func (*ExecResponse) Descriptor() ([]byte, []int) {
|
||||
return file_hostagent_proto_rawDescGZIP(), []int{9}
|
||||
}
|
||||
|
||||
func (x *ExecResponse) GetStdout() []byte {
|
||||
if x != nil {
|
||||
return x.Stdout
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ExecResponse) GetStderr() []byte {
|
||||
if x != nil {
|
||||
return x.Stderr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ExecResponse) GetExitCode() int32 {
|
||||
if x != nil {
|
||||
return x.ExitCode
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type ListSandboxesRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ListSandboxesRequest) Reset() {
|
||||
*x = ListSandboxesRequest{}
|
||||
mi := &file_hostagent_proto_msgTypes[10]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ListSandboxesRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ListSandboxesRequest) ProtoMessage() {}
|
||||
|
||||
func (x *ListSandboxesRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_hostagent_proto_msgTypes[10]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ListSandboxesRequest.ProtoReflect.Descriptor instead.
|
||||
func (*ListSandboxesRequest) Descriptor() ([]byte, []int) {
|
||||
return file_hostagent_proto_rawDescGZIP(), []int{10}
|
||||
}
|
||||
|
||||
type ListSandboxesResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Sandboxes []*SandboxInfo `protobuf:"bytes,1,rep,name=sandboxes,proto3" json:"sandboxes,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ListSandboxesResponse) Reset() {
|
||||
*x = ListSandboxesResponse{}
|
||||
mi := &file_hostagent_proto_msgTypes[11]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ListSandboxesResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ListSandboxesResponse) ProtoMessage() {}
|
||||
|
||||
func (x *ListSandboxesResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_hostagent_proto_msgTypes[11]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ListSandboxesResponse.ProtoReflect.Descriptor instead.
|
||||
func (*ListSandboxesResponse) Descriptor() ([]byte, []int) {
|
||||
return file_hostagent_proto_rawDescGZIP(), []int{11}
|
||||
}
|
||||
|
||||
func (x *ListSandboxesResponse) GetSandboxes() []*SandboxInfo {
|
||||
if x != nil {
|
||||
return x.Sandboxes
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type SandboxInfo struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
SandboxId string `protobuf:"bytes,1,opt,name=sandbox_id,json=sandboxId,proto3" json:"sandbox_id,omitempty"`
|
||||
Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"`
|
||||
Template string `protobuf:"bytes,3,opt,name=template,proto3" json:"template,omitempty"`
|
||||
Vcpus int32 `protobuf:"varint,4,opt,name=vcpus,proto3" json:"vcpus,omitempty"`
|
||||
MemoryMb int32 `protobuf:"varint,5,opt,name=memory_mb,json=memoryMb,proto3" json:"memory_mb,omitempty"`
|
||||
HostIp string `protobuf:"bytes,6,opt,name=host_ip,json=hostIp,proto3" json:"host_ip,omitempty"`
|
||||
CreatedAtUnix int64 `protobuf:"varint,7,opt,name=created_at_unix,json=createdAtUnix,proto3" json:"created_at_unix,omitempty"`
|
||||
LastActiveAtUnix int64 `protobuf:"varint,8,opt,name=last_active_at_unix,json=lastActiveAtUnix,proto3" json:"last_active_at_unix,omitempty"`
|
||||
TimeoutSec int32 `protobuf:"varint,9,opt,name=timeout_sec,json=timeoutSec,proto3" json:"timeout_sec,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *SandboxInfo) Reset() {
|
||||
*x = SandboxInfo{}
|
||||
mi := &file_hostagent_proto_msgTypes[12]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *SandboxInfo) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SandboxInfo) ProtoMessage() {}
|
||||
|
||||
func (x *SandboxInfo) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_hostagent_proto_msgTypes[12]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SandboxInfo.ProtoReflect.Descriptor instead.
|
||||
func (*SandboxInfo) Descriptor() ([]byte, []int) {
|
||||
return file_hostagent_proto_rawDescGZIP(), []int{12}
|
||||
}
|
||||
|
||||
func (x *SandboxInfo) GetSandboxId() string {
|
||||
if x != nil {
|
||||
return x.SandboxId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *SandboxInfo) GetStatus() string {
|
||||
if x != nil {
|
||||
return x.Status
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *SandboxInfo) GetTemplate() string {
|
||||
if x != nil {
|
||||
return x.Template
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *SandboxInfo) GetVcpus() int32 {
|
||||
if x != nil {
|
||||
return x.Vcpus
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *SandboxInfo) GetMemoryMb() int32 {
|
||||
if x != nil {
|
||||
return x.MemoryMb
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *SandboxInfo) GetHostIp() string {
|
||||
if x != nil {
|
||||
return x.HostIp
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *SandboxInfo) GetCreatedAtUnix() int64 {
|
||||
if x != nil {
|
||||
return x.CreatedAtUnix
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *SandboxInfo) GetLastActiveAtUnix() int64 {
|
||||
if x != nil {
|
||||
return x.LastActiveAtUnix
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *SandboxInfo) GetTimeoutSec() int32 {
|
||||
if x != nil {
|
||||
return x.TimeoutSec
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
var File_hostagent_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_hostagent_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x0fhostagent.proto\x12\fhostagent.v1\"\x86\x01\n" +
|
||||
"\x14CreateSandboxRequest\x12\x1a\n" +
|
||||
"\btemplate\x18\x01 \x01(\tR\btemplate\x12\x14\n" +
|
||||
"\x05vcpus\x18\x02 \x01(\x05R\x05vcpus\x12\x1b\n" +
|
||||
"\tmemory_mb\x18\x03 \x01(\x05R\bmemoryMb\x12\x1f\n" +
|
||||
"\vtimeout_sec\x18\x04 \x01(\x05R\n" +
|
||||
"timeoutSec\"g\n" +
|
||||
"\x15CreateSandboxResponse\x12\x1d\n" +
|
||||
"\n" +
|
||||
"sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x16\n" +
|
||||
"\x06status\x18\x02 \x01(\tR\x06status\x12\x17\n" +
|
||||
"\ahost_ip\x18\x03 \x01(\tR\x06hostIp\"6\n" +
|
||||
"\x15DestroySandboxRequest\x12\x1d\n" +
|
||||
"\n" +
|
||||
"sandbox_id\x18\x01 \x01(\tR\tsandboxId\"\x18\n" +
|
||||
"\x16DestroySandboxResponse\"4\n" +
|
||||
"\x13PauseSandboxRequest\x12\x1d\n" +
|
||||
"\n" +
|
||||
"sandbox_id\x18\x01 \x01(\tR\tsandboxId\"\x16\n" +
|
||||
"\x14PauseSandboxResponse\"5\n" +
|
||||
"\x14ResumeSandboxRequest\x12\x1d\n" +
|
||||
"\n" +
|
||||
"sandbox_id\x18\x01 \x01(\tR\tsandboxId\"\x17\n" +
|
||||
"\x15ResumeSandboxResponse\"s\n" +
|
||||
"\vExecRequest\x12\x1d\n" +
|
||||
"\n" +
|
||||
"sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x10\n" +
|
||||
"\x03cmd\x18\x02 \x01(\tR\x03cmd\x12\x12\n" +
|
||||
"\x04args\x18\x03 \x03(\tR\x04args\x12\x1f\n" +
|
||||
"\vtimeout_sec\x18\x04 \x01(\x05R\n" +
|
||||
"timeoutSec\"[\n" +
|
||||
"\fExecResponse\x12\x16\n" +
|
||||
"\x06stdout\x18\x01 \x01(\fR\x06stdout\x12\x16\n" +
|
||||
"\x06stderr\x18\x02 \x01(\fR\x06stderr\x12\x1b\n" +
|
||||
"\texit_code\x18\x03 \x01(\x05R\bexitCode\"\x16\n" +
|
||||
"\x14ListSandboxesRequest\"P\n" +
|
||||
"\x15ListSandboxesResponse\x127\n" +
|
||||
"\tsandboxes\x18\x01 \x03(\v2\x19.hostagent.v1.SandboxInfoR\tsandboxes\"\xa4\x02\n" +
|
||||
"\vSandboxInfo\x12\x1d\n" +
|
||||
"\n" +
|
||||
"sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x16\n" +
|
||||
"\x06status\x18\x02 \x01(\tR\x06status\x12\x1a\n" +
|
||||
"\btemplate\x18\x03 \x01(\tR\btemplate\x12\x14\n" +
|
||||
"\x05vcpus\x18\x04 \x01(\x05R\x05vcpus\x12\x1b\n" +
|
||||
"\tmemory_mb\x18\x05 \x01(\x05R\bmemoryMb\x12\x17\n" +
|
||||
"\ahost_ip\x18\x06 \x01(\tR\x06hostIp\x12&\n" +
|
||||
"\x0fcreated_at_unix\x18\a \x01(\x03R\rcreatedAtUnix\x12-\n" +
|
||||
"\x13last_active_at_unix\x18\b \x01(\x03R\x10lastActiveAtUnix\x12\x1f\n" +
|
||||
"\vtimeout_sec\x18\t \x01(\x05R\n" +
|
||||
"timeoutSec2\x93\x04\n" +
|
||||
"\x10HostAgentService\x12X\n" +
|
||||
"\rCreateSandbox\x12\".hostagent.v1.CreateSandboxRequest\x1a#.hostagent.v1.CreateSandboxResponse\x12[\n" +
|
||||
"\x0eDestroySandbox\x12#.hostagent.v1.DestroySandboxRequest\x1a$.hostagent.v1.DestroySandboxResponse\x12U\n" +
|
||||
"\fPauseSandbox\x12!.hostagent.v1.PauseSandboxRequest\x1a\".hostagent.v1.PauseSandboxResponse\x12X\n" +
|
||||
"\rResumeSandbox\x12\".hostagent.v1.ResumeSandboxRequest\x1a#.hostagent.v1.ResumeSandboxResponse\x12=\n" +
|
||||
"\x04Exec\x12\x19.hostagent.v1.ExecRequest\x1a\x1a.hostagent.v1.ExecResponse\x12X\n" +
|
||||
"\rListSandboxes\x12\".hostagent.v1.ListSandboxesRequest\x1a#.hostagent.v1.ListSandboxesResponseB\xb0\x01\n" +
|
||||
"\x10com.hostagent.v1B\x0eHostagentProtoP\x01Z;git.omukk.dev/wrenn/sandbox/proto/hostagent/gen;hostagentv1\xa2\x02\x03HXX\xaa\x02\fHostagent.V1\xca\x02\fHostagent\\V1\xe2\x02\x18Hostagent\\V1\\GPBMetadata\xea\x02\rHostagent::V1b\x06proto3"
|
||||
|
||||
var (
|
||||
file_hostagent_proto_rawDescOnce sync.Once
|
||||
file_hostagent_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_hostagent_proto_rawDescGZIP() []byte {
|
||||
file_hostagent_proto_rawDescOnce.Do(func() {
|
||||
file_hostagent_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_hostagent_proto_rawDesc), len(file_hostagent_proto_rawDesc)))
|
||||
})
|
||||
return file_hostagent_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_hostagent_proto_msgTypes = make([]protoimpl.MessageInfo, 13)
|
||||
var file_hostagent_proto_goTypes = []any{
|
||||
(*CreateSandboxRequest)(nil), // 0: hostagent.v1.CreateSandboxRequest
|
||||
(*CreateSandboxResponse)(nil), // 1: hostagent.v1.CreateSandboxResponse
|
||||
(*DestroySandboxRequest)(nil), // 2: hostagent.v1.DestroySandboxRequest
|
||||
(*DestroySandboxResponse)(nil), // 3: hostagent.v1.DestroySandboxResponse
|
||||
(*PauseSandboxRequest)(nil), // 4: hostagent.v1.PauseSandboxRequest
|
||||
(*PauseSandboxResponse)(nil), // 5: hostagent.v1.PauseSandboxResponse
|
||||
(*ResumeSandboxRequest)(nil), // 6: hostagent.v1.ResumeSandboxRequest
|
||||
(*ResumeSandboxResponse)(nil), // 7: hostagent.v1.ResumeSandboxResponse
|
||||
(*ExecRequest)(nil), // 8: hostagent.v1.ExecRequest
|
||||
(*ExecResponse)(nil), // 9: hostagent.v1.ExecResponse
|
||||
(*ListSandboxesRequest)(nil), // 10: hostagent.v1.ListSandboxesRequest
|
||||
(*ListSandboxesResponse)(nil), // 11: hostagent.v1.ListSandboxesResponse
|
||||
(*SandboxInfo)(nil), // 12: hostagent.v1.SandboxInfo
|
||||
}
|
||||
var file_hostagent_proto_depIdxs = []int32{
|
||||
12, // 0: hostagent.v1.ListSandboxesResponse.sandboxes:type_name -> hostagent.v1.SandboxInfo
|
||||
0, // 1: hostagent.v1.HostAgentService.CreateSandbox:input_type -> hostagent.v1.CreateSandboxRequest
|
||||
2, // 2: hostagent.v1.HostAgentService.DestroySandbox:input_type -> hostagent.v1.DestroySandboxRequest
|
||||
4, // 3: hostagent.v1.HostAgentService.PauseSandbox:input_type -> hostagent.v1.PauseSandboxRequest
|
||||
6, // 4: hostagent.v1.HostAgentService.ResumeSandbox:input_type -> hostagent.v1.ResumeSandboxRequest
|
||||
8, // 5: hostagent.v1.HostAgentService.Exec:input_type -> hostagent.v1.ExecRequest
|
||||
10, // 6: hostagent.v1.HostAgentService.ListSandboxes:input_type -> hostagent.v1.ListSandboxesRequest
|
||||
1, // 7: hostagent.v1.HostAgentService.CreateSandbox:output_type -> hostagent.v1.CreateSandboxResponse
|
||||
3, // 8: hostagent.v1.HostAgentService.DestroySandbox:output_type -> hostagent.v1.DestroySandboxResponse
|
||||
5, // 9: hostagent.v1.HostAgentService.PauseSandbox:output_type -> hostagent.v1.PauseSandboxResponse
|
||||
7, // 10: hostagent.v1.HostAgentService.ResumeSandbox:output_type -> hostagent.v1.ResumeSandboxResponse
|
||||
9, // 11: hostagent.v1.HostAgentService.Exec:output_type -> hostagent.v1.ExecResponse
|
||||
11, // 12: hostagent.v1.HostAgentService.ListSandboxes:output_type -> hostagent.v1.ListSandboxesResponse
|
||||
7, // [7:13] is the sub-list for method output_type
|
||||
1, // [1:7] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_hostagent_proto_init() }
|
||||
func file_hostagent_proto_init() {
|
||||
if File_hostagent_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_hostagent_proto_rawDesc), len(file_hostagent_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 13,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_hostagent_proto_goTypes,
|
||||
DependencyIndexes: file_hostagent_proto_depIdxs,
|
||||
MessageInfos: file_hostagent_proto_msgTypes,
|
||||
}.Build()
|
||||
File_hostagent_proto = out.File
|
||||
file_hostagent_proto_goTypes = nil
|
||||
file_hostagent_proto_depIdxs = nil
|
||||
}
|
||||
265
proto/hostagent/gen/hostagentv1connect/hostagent.connect.go
Normal file
265
proto/hostagent/gen/hostagentv1connect/hostagent.connect.go
Normal file
@ -0,0 +1,265 @@
|
||||
// Code generated by protoc-gen-connect-go. DO NOT EDIT.
|
||||
//
|
||||
// Source: hostagent.proto
|
||||
|
||||
package hostagentv1connect
|
||||
|
||||
import (
|
||||
connect "connectrpc.com/connect"
|
||||
context "context"
|
||||
errors "errors"
|
||||
gen "git.omukk.dev/wrenn/sandbox/proto/hostagent/gen"
|
||||
http "net/http"
|
||||
strings "strings"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file and the connect package are
|
||||
// compatible. If you get a compiler error that this constant is not defined, this code was
|
||||
// generated with a version of connect newer than the one compiled into your binary. You can fix the
|
||||
// problem by either regenerating this code with an older version of connect or updating the connect
|
||||
// version compiled into your binary.
|
||||
const _ = connect.IsAtLeastVersion1_13_0
|
||||
|
||||
const (
|
||||
// HostAgentServiceName is the fully-qualified name of the HostAgentService service.
|
||||
HostAgentServiceName = "hostagent.v1.HostAgentService"
|
||||
)
|
||||
|
||||
// These constants are the fully-qualified names of the RPCs defined in this package. They're
|
||||
// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.
|
||||
//
|
||||
// Note that these are different from the fully-qualified method names used by
|
||||
// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
|
||||
// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
|
||||
// period.
|
||||
const (
|
||||
// HostAgentServiceCreateSandboxProcedure is the fully-qualified name of the HostAgentService's
|
||||
// CreateSandbox RPC.
|
||||
HostAgentServiceCreateSandboxProcedure = "/hostagent.v1.HostAgentService/CreateSandbox"
|
||||
// HostAgentServiceDestroySandboxProcedure is the fully-qualified name of the HostAgentService's
|
||||
// DestroySandbox RPC.
|
||||
HostAgentServiceDestroySandboxProcedure = "/hostagent.v1.HostAgentService/DestroySandbox"
|
||||
// HostAgentServicePauseSandboxProcedure is the fully-qualified name of the HostAgentService's
|
||||
// PauseSandbox RPC.
|
||||
HostAgentServicePauseSandboxProcedure = "/hostagent.v1.HostAgentService/PauseSandbox"
|
||||
// HostAgentServiceResumeSandboxProcedure is the fully-qualified name of the HostAgentService's
|
||||
// ResumeSandbox RPC.
|
||||
HostAgentServiceResumeSandboxProcedure = "/hostagent.v1.HostAgentService/ResumeSandbox"
|
||||
// HostAgentServiceExecProcedure is the fully-qualified name of the HostAgentService's Exec RPC.
|
||||
HostAgentServiceExecProcedure = "/hostagent.v1.HostAgentService/Exec"
|
||||
// HostAgentServiceListSandboxesProcedure is the fully-qualified name of the HostAgentService's
|
||||
// ListSandboxes RPC.
|
||||
HostAgentServiceListSandboxesProcedure = "/hostagent.v1.HostAgentService/ListSandboxes"
|
||||
)
|
||||
|
||||
// HostAgentServiceClient is a client for the hostagent.v1.HostAgentService service.
|
||||
type HostAgentServiceClient interface {
|
||||
// CreateSandbox boots a new microVM with the given configuration.
|
||||
CreateSandbox(context.Context, *connect.Request[gen.CreateSandboxRequest]) (*connect.Response[gen.CreateSandboxResponse], error)
|
||||
// DestroySandbox stops and cleans up a sandbox (VM, network, rootfs).
|
||||
DestroySandbox(context.Context, *connect.Request[gen.DestroySandboxRequest]) (*connect.Response[gen.DestroySandboxResponse], error)
|
||||
// PauseSandbox pauses a running sandbox's VM.
|
||||
PauseSandbox(context.Context, *connect.Request[gen.PauseSandboxRequest]) (*connect.Response[gen.PauseSandboxResponse], error)
|
||||
// ResumeSandbox resumes a paused sandbox's VM.
|
||||
ResumeSandbox(context.Context, *connect.Request[gen.ResumeSandboxRequest]) (*connect.Response[gen.ResumeSandboxResponse], error)
|
||||
// Exec runs a command inside a sandbox and returns the collected output.
|
||||
Exec(context.Context, *connect.Request[gen.ExecRequest]) (*connect.Response[gen.ExecResponse], error)
|
||||
// ListSandboxes returns all sandboxes managed by this host agent.
|
||||
ListSandboxes(context.Context, *connect.Request[gen.ListSandboxesRequest]) (*connect.Response[gen.ListSandboxesResponse], error)
|
||||
}
|
||||
|
||||
// NewHostAgentServiceClient constructs a client for the hostagent.v1.HostAgentService service. By
|
||||
// default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses,
|
||||
// and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the
|
||||
// connect.WithGRPC() or connect.WithGRPCWeb() options.
|
||||
//
|
||||
// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
|
||||
// http://api.acme.com or https://acme.com/grpc).
|
||||
func NewHostAgentServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) HostAgentServiceClient {
|
||||
baseURL = strings.TrimRight(baseURL, "/")
|
||||
hostAgentServiceMethods := gen.File_hostagent_proto.Services().ByName("HostAgentService").Methods()
|
||||
return &hostAgentServiceClient{
|
||||
createSandbox: connect.NewClient[gen.CreateSandboxRequest, gen.CreateSandboxResponse](
|
||||
httpClient,
|
||||
baseURL+HostAgentServiceCreateSandboxProcedure,
|
||||
connect.WithSchema(hostAgentServiceMethods.ByName("CreateSandbox")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
destroySandbox: connect.NewClient[gen.DestroySandboxRequest, gen.DestroySandboxResponse](
|
||||
httpClient,
|
||||
baseURL+HostAgentServiceDestroySandboxProcedure,
|
||||
connect.WithSchema(hostAgentServiceMethods.ByName("DestroySandbox")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
pauseSandbox: connect.NewClient[gen.PauseSandboxRequest, gen.PauseSandboxResponse](
|
||||
httpClient,
|
||||
baseURL+HostAgentServicePauseSandboxProcedure,
|
||||
connect.WithSchema(hostAgentServiceMethods.ByName("PauseSandbox")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
resumeSandbox: connect.NewClient[gen.ResumeSandboxRequest, gen.ResumeSandboxResponse](
|
||||
httpClient,
|
||||
baseURL+HostAgentServiceResumeSandboxProcedure,
|
||||
connect.WithSchema(hostAgentServiceMethods.ByName("ResumeSandbox")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
exec: connect.NewClient[gen.ExecRequest, gen.ExecResponse](
|
||||
httpClient,
|
||||
baseURL+HostAgentServiceExecProcedure,
|
||||
connect.WithSchema(hostAgentServiceMethods.ByName("Exec")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
listSandboxes: connect.NewClient[gen.ListSandboxesRequest, gen.ListSandboxesResponse](
|
||||
httpClient,
|
||||
baseURL+HostAgentServiceListSandboxesProcedure,
|
||||
connect.WithSchema(hostAgentServiceMethods.ByName("ListSandboxes")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// hostAgentServiceClient implements HostAgentServiceClient.
|
||||
type hostAgentServiceClient struct {
|
||||
createSandbox *connect.Client[gen.CreateSandboxRequest, gen.CreateSandboxResponse]
|
||||
destroySandbox *connect.Client[gen.DestroySandboxRequest, gen.DestroySandboxResponse]
|
||||
pauseSandbox *connect.Client[gen.PauseSandboxRequest, gen.PauseSandboxResponse]
|
||||
resumeSandbox *connect.Client[gen.ResumeSandboxRequest, gen.ResumeSandboxResponse]
|
||||
exec *connect.Client[gen.ExecRequest, gen.ExecResponse]
|
||||
listSandboxes *connect.Client[gen.ListSandboxesRequest, gen.ListSandboxesResponse]
|
||||
}
|
||||
|
||||
// CreateSandbox calls hostagent.v1.HostAgentService.CreateSandbox.
|
||||
func (c *hostAgentServiceClient) CreateSandbox(ctx context.Context, req *connect.Request[gen.CreateSandboxRequest]) (*connect.Response[gen.CreateSandboxResponse], error) {
|
||||
return c.createSandbox.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// DestroySandbox calls hostagent.v1.HostAgentService.DestroySandbox.
|
||||
func (c *hostAgentServiceClient) DestroySandbox(ctx context.Context, req *connect.Request[gen.DestroySandboxRequest]) (*connect.Response[gen.DestroySandboxResponse], error) {
|
||||
return c.destroySandbox.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// PauseSandbox calls hostagent.v1.HostAgentService.PauseSandbox.
|
||||
func (c *hostAgentServiceClient) PauseSandbox(ctx context.Context, req *connect.Request[gen.PauseSandboxRequest]) (*connect.Response[gen.PauseSandboxResponse], error) {
|
||||
return c.pauseSandbox.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// ResumeSandbox calls hostagent.v1.HostAgentService.ResumeSandbox.
|
||||
func (c *hostAgentServiceClient) ResumeSandbox(ctx context.Context, req *connect.Request[gen.ResumeSandboxRequest]) (*connect.Response[gen.ResumeSandboxResponse], error) {
|
||||
return c.resumeSandbox.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// Exec calls hostagent.v1.HostAgentService.Exec.
|
||||
func (c *hostAgentServiceClient) Exec(ctx context.Context, req *connect.Request[gen.ExecRequest]) (*connect.Response[gen.ExecResponse], error) {
|
||||
return c.exec.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// ListSandboxes calls hostagent.v1.HostAgentService.ListSandboxes.
|
||||
func (c *hostAgentServiceClient) ListSandboxes(ctx context.Context, req *connect.Request[gen.ListSandboxesRequest]) (*connect.Response[gen.ListSandboxesResponse], error) {
|
||||
return c.listSandboxes.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// HostAgentServiceHandler is an implementation of the hostagent.v1.HostAgentService service.
|
||||
type HostAgentServiceHandler interface {
|
||||
// CreateSandbox boots a new microVM with the given configuration.
|
||||
CreateSandbox(context.Context, *connect.Request[gen.CreateSandboxRequest]) (*connect.Response[gen.CreateSandboxResponse], error)
|
||||
// DestroySandbox stops and cleans up a sandbox (VM, network, rootfs).
|
||||
DestroySandbox(context.Context, *connect.Request[gen.DestroySandboxRequest]) (*connect.Response[gen.DestroySandboxResponse], error)
|
||||
// PauseSandbox pauses a running sandbox's VM.
|
||||
PauseSandbox(context.Context, *connect.Request[gen.PauseSandboxRequest]) (*connect.Response[gen.PauseSandboxResponse], error)
|
||||
// ResumeSandbox resumes a paused sandbox's VM.
|
||||
ResumeSandbox(context.Context, *connect.Request[gen.ResumeSandboxRequest]) (*connect.Response[gen.ResumeSandboxResponse], error)
|
||||
// Exec runs a command inside a sandbox and returns the collected output.
|
||||
Exec(context.Context, *connect.Request[gen.ExecRequest]) (*connect.Response[gen.ExecResponse], error)
|
||||
// ListSandboxes returns all sandboxes managed by this host agent.
|
||||
ListSandboxes(context.Context, *connect.Request[gen.ListSandboxesRequest]) (*connect.Response[gen.ListSandboxesResponse], error)
|
||||
}
|
||||
|
||||
// NewHostAgentServiceHandler builds an HTTP handler from the service implementation. It returns the
|
||||
// path on which to mount the handler and the handler itself.
|
||||
//
|
||||
// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
|
||||
// and JSON codecs. They also support gzip compression.
|
||||
func NewHostAgentServiceHandler(svc HostAgentServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
|
||||
hostAgentServiceMethods := gen.File_hostagent_proto.Services().ByName("HostAgentService").Methods()
|
||||
hostAgentServiceCreateSandboxHandler := connect.NewUnaryHandler(
|
||||
HostAgentServiceCreateSandboxProcedure,
|
||||
svc.CreateSandbox,
|
||||
connect.WithSchema(hostAgentServiceMethods.ByName("CreateSandbox")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
hostAgentServiceDestroySandboxHandler := connect.NewUnaryHandler(
|
||||
HostAgentServiceDestroySandboxProcedure,
|
||||
svc.DestroySandbox,
|
||||
connect.WithSchema(hostAgentServiceMethods.ByName("DestroySandbox")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
hostAgentServicePauseSandboxHandler := connect.NewUnaryHandler(
|
||||
HostAgentServicePauseSandboxProcedure,
|
||||
svc.PauseSandbox,
|
||||
connect.WithSchema(hostAgentServiceMethods.ByName("PauseSandbox")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
hostAgentServiceResumeSandboxHandler := connect.NewUnaryHandler(
|
||||
HostAgentServiceResumeSandboxProcedure,
|
||||
svc.ResumeSandbox,
|
||||
connect.WithSchema(hostAgentServiceMethods.ByName("ResumeSandbox")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
hostAgentServiceExecHandler := connect.NewUnaryHandler(
|
||||
HostAgentServiceExecProcedure,
|
||||
svc.Exec,
|
||||
connect.WithSchema(hostAgentServiceMethods.ByName("Exec")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
hostAgentServiceListSandboxesHandler := connect.NewUnaryHandler(
|
||||
HostAgentServiceListSandboxesProcedure,
|
||||
svc.ListSandboxes,
|
||||
connect.WithSchema(hostAgentServiceMethods.ByName("ListSandboxes")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
return "/hostagent.v1.HostAgentService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case HostAgentServiceCreateSandboxProcedure:
|
||||
hostAgentServiceCreateSandboxHandler.ServeHTTP(w, r)
|
||||
case HostAgentServiceDestroySandboxProcedure:
|
||||
hostAgentServiceDestroySandboxHandler.ServeHTTP(w, r)
|
||||
case HostAgentServicePauseSandboxProcedure:
|
||||
hostAgentServicePauseSandboxHandler.ServeHTTP(w, r)
|
||||
case HostAgentServiceResumeSandboxProcedure:
|
||||
hostAgentServiceResumeSandboxHandler.ServeHTTP(w, r)
|
||||
case HostAgentServiceExecProcedure:
|
||||
hostAgentServiceExecHandler.ServeHTTP(w, r)
|
||||
case HostAgentServiceListSandboxesProcedure:
|
||||
hostAgentServiceListSandboxesHandler.ServeHTTP(w, r)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// UnimplementedHostAgentServiceHandler returns CodeUnimplemented from all methods.
|
||||
type UnimplementedHostAgentServiceHandler struct{}
|
||||
|
||||
func (UnimplementedHostAgentServiceHandler) CreateSandbox(context.Context, *connect.Request[gen.CreateSandboxRequest]) (*connect.Response[gen.CreateSandboxResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hostagent.v1.HostAgentService.CreateSandbox is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedHostAgentServiceHandler) DestroySandbox(context.Context, *connect.Request[gen.DestroySandboxRequest]) (*connect.Response[gen.DestroySandboxResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hostagent.v1.HostAgentService.DestroySandbox is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedHostAgentServiceHandler) PauseSandbox(context.Context, *connect.Request[gen.PauseSandboxRequest]) (*connect.Response[gen.PauseSandboxResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hostagent.v1.HostAgentService.PauseSandbox is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedHostAgentServiceHandler) ResumeSandbox(context.Context, *connect.Request[gen.ResumeSandboxRequest]) (*connect.Response[gen.ResumeSandboxResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hostagent.v1.HostAgentService.ResumeSandbox is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedHostAgentServiceHandler) Exec(context.Context, *connect.Request[gen.ExecRequest]) (*connect.Response[gen.ExecResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hostagent.v1.HostAgentService.Exec is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedHostAgentServiceHandler) ListSandboxes(context.Context, *connect.Request[gen.ListSandboxesRequest]) (*connect.Response[gen.ListSandboxesResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hostagent.v1.HostAgentService.ListSandboxes is not implemented"))
|
||||
}
|
||||
@ -0,0 +1,96 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package hostagent.v1;
|
||||
|
||||
// HostAgentService manages sandbox VMs on a single physical host.
|
||||
// The control plane calls these RPCs to orchestrate sandbox lifecycle.
|
||||
service HostAgentService {
|
||||
// CreateSandbox boots a new microVM with the given configuration.
|
||||
rpc CreateSandbox(CreateSandboxRequest) returns (CreateSandboxResponse);
|
||||
|
||||
// DestroySandbox stops and cleans up a sandbox (VM, network, rootfs).
|
||||
rpc DestroySandbox(DestroySandboxRequest) returns (DestroySandboxResponse);
|
||||
|
||||
// PauseSandbox pauses a running sandbox's VM.
|
||||
rpc PauseSandbox(PauseSandboxRequest) returns (PauseSandboxResponse);
|
||||
|
||||
// ResumeSandbox resumes a paused sandbox's VM.
|
||||
rpc ResumeSandbox(ResumeSandboxRequest) returns (ResumeSandboxResponse);
|
||||
|
||||
// Exec runs a command inside a sandbox and returns the collected output.
|
||||
rpc Exec(ExecRequest) returns (ExecResponse);
|
||||
|
||||
// ListSandboxes returns all sandboxes managed by this host agent.
|
||||
rpc ListSandboxes(ListSandboxesRequest) returns (ListSandboxesResponse);
|
||||
}
|
||||
|
||||
message CreateSandboxRequest {
|
||||
// Template name (e.g., "minimal", "python311"). Determines base rootfs.
|
||||
string template = 1;
|
||||
|
||||
// Number of virtual CPUs (default: 1).
|
||||
int32 vcpus = 2;
|
||||
|
||||
// Memory in MB (default: 512).
|
||||
int32 memory_mb = 3;
|
||||
|
||||
// TTL in seconds. Sandbox is auto-destroyed after this duration of
|
||||
// inactivity. 0 means no auto-destroy.
|
||||
int32 timeout_sec = 4;
|
||||
}
|
||||
|
||||
message CreateSandboxResponse {
|
||||
string sandbox_id = 1;
|
||||
string status = 2;
|
||||
string host_ip = 3;
|
||||
}
|
||||
|
||||
message DestroySandboxRequest {
|
||||
string sandbox_id = 1;
|
||||
}
|
||||
|
||||
message DestroySandboxResponse {}
|
||||
|
||||
message PauseSandboxRequest {
|
||||
string sandbox_id = 1;
|
||||
}
|
||||
|
||||
message PauseSandboxResponse {}
|
||||
|
||||
message ResumeSandboxRequest {
|
||||
string sandbox_id = 1;
|
||||
}
|
||||
|
||||
message ResumeSandboxResponse {}
|
||||
|
||||
message ExecRequest {
|
||||
string sandbox_id = 1;
|
||||
string cmd = 2;
|
||||
repeated string args = 3;
|
||||
// Timeout for the command in seconds (default: 30).
|
||||
int32 timeout_sec = 4;
|
||||
}
|
||||
|
||||
message ExecResponse {
|
||||
bytes stdout = 1;
|
||||
bytes stderr = 2;
|
||||
int32 exit_code = 3;
|
||||
}
|
||||
|
||||
message ListSandboxesRequest {}
|
||||
|
||||
message ListSandboxesResponse {
|
||||
repeated SandboxInfo sandboxes = 1;
|
||||
}
|
||||
|
||||
message SandboxInfo {
|
||||
string sandbox_id = 1;
|
||||
string status = 2;
|
||||
string template = 3;
|
||||
int32 vcpus = 4;
|
||||
int32 memory_mb = 5;
|
||||
string host_ip = 6;
|
||||
int64 created_at_unix = 7;
|
||||
int64 last_active_at_unix = 8;
|
||||
int32 timeout_sec = 9;
|
||||
}
|
||||
|
||||
233
scripts/test-host.sh
Executable file
233
scripts/test-host.sh
Executable file
@ -0,0 +1,233 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# test-host.sh — Integration test for the Wrenn host agent.
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Host agent running: sudo ./builds/wrenn-agent
|
||||
# - Firecracker installed at /usr/local/bin/firecracker
|
||||
# - Kernel at /var/lib/wrenn/kernels/vmlinux
|
||||
# - Base rootfs at /var/lib/wrenn/images/minimal.ext4 (with envd + wrenn-init baked in)
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/test-host.sh [agent_url]
|
||||
#
|
||||
# The agent URL defaults to http://localhost:50051.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
AGENT="${1:-http://localhost:50051}"
|
||||
BASE="/hostagent.v1.HostAgentService"
|
||||
SANDBOX_ID=""
|
||||
|
||||
# Colors for output.
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
pass() { echo -e "${GREEN}PASS${NC}: $1"; }
|
||||
fail() { echo -e "${RED}FAIL${NC}: $1"; exit 1; }
|
||||
info() { echo -e "${YELLOW}----${NC}: $1"; }
|
||||
|
||||
rpc() {
|
||||
local method="$1"
|
||||
local body="$2"
|
||||
curl -s -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
"${AGENT}${BASE}/${method}" \
|
||||
-d "${body}"
|
||||
}
|
||||
|
||||
# ──────────────────────────────────────────────────
|
||||
# Test 1: List sandboxes (should be empty)
|
||||
# ──────────────────────────────────────────────────
|
||||
info "Test 1: List sandboxes (expect empty)"
|
||||
|
||||
RESULT=$(rpc "ListSandboxes" '{}')
|
||||
echo " Response: ${RESULT}"
|
||||
|
||||
echo "${RESULT}" | grep -q '"sandboxes"' || echo "${RESULT}" | grep -q '{}' && \
|
||||
pass "ListSandboxes returned" || \
|
||||
fail "ListSandboxes failed"
|
||||
|
||||
# ──────────────────────────────────────────────────
|
||||
# Test 2: Create a sandbox
|
||||
# ──────────────────────────────────────────────────
|
||||
info "Test 2: Create a sandbox"
|
||||
|
||||
RESULT=$(rpc "CreateSandbox" '{
|
||||
"template": "minimal",
|
||||
"vcpus": 1,
|
||||
"memoryMb": 512,
|
||||
"timeoutSec": 300
|
||||
}')
|
||||
echo " Response: ${RESULT}"
|
||||
|
||||
SANDBOX_ID=$(echo "${RESULT}" | python3 -c "import sys,json; print(json.load(sys.stdin)['sandboxId'])" 2>/dev/null) || \
|
||||
fail "CreateSandbox did not return sandboxId"
|
||||
|
||||
echo " Sandbox ID: ${SANDBOX_ID}"
|
||||
pass "Sandbox created: ${SANDBOX_ID}"
|
||||
|
||||
# ──────────────────────────────────────────────────
|
||||
# Test 3: List sandboxes (should have one)
|
||||
# ──────────────────────────────────────────────────
|
||||
info "Test 3: List sandboxes (expect one)"
|
||||
|
||||
RESULT=$(rpc "ListSandboxes" '{}')
|
||||
echo " Response: ${RESULT}"
|
||||
|
||||
echo "${RESULT}" | grep -q "${SANDBOX_ID}" && \
|
||||
pass "Sandbox ${SANDBOX_ID} found in list" || \
|
||||
fail "Sandbox not found in list"
|
||||
|
||||
# ──────────────────────────────────────────────────
|
||||
# Test 4: Execute a command
|
||||
# ──────────────────────────────────────────────────
|
||||
info "Test 4: Execute 'echo hello world'"
|
||||
|
||||
RESULT=$(rpc "Exec" "{
|
||||
\"sandboxId\": \"${SANDBOX_ID}\",
|
||||
\"cmd\": \"/bin/sh\",
|
||||
\"args\": [\"-c\", \"echo hello world\"],
|
||||
\"timeoutSec\": 10
|
||||
}")
|
||||
echo " Response: ${RESULT}"
|
||||
|
||||
# stdout is base64-encoded in Connect RPC JSON.
|
||||
STDOUT=$(echo "${RESULT}" | python3 -c "
|
||||
import sys, json, base64
|
||||
r = json.load(sys.stdin)
|
||||
print(base64.b64decode(r['stdout']).decode().strip())
|
||||
" 2>/dev/null) || fail "Exec did not return stdout"
|
||||
|
||||
[ "${STDOUT}" = "hello world" ] && \
|
||||
pass "Exec returned correct output: '${STDOUT}'" || \
|
||||
fail "Expected 'hello world', got '${STDOUT}'"
|
||||
|
||||
# ──────────────────────────────────────────────────
|
||||
# Test 5: Execute a multi-line command
|
||||
# ──────────────────────────────────────────────────
|
||||
info "Test 5: Execute multi-line command"
|
||||
|
||||
RESULT=$(rpc "Exec" "{
|
||||
\"sandboxId\": \"${SANDBOX_ID}\",
|
||||
\"cmd\": \"/bin/sh\",
|
||||
\"args\": [\"-c\", \"echo line1; echo line2; echo line3\"],
|
||||
\"timeoutSec\": 10
|
||||
}")
|
||||
echo " Response: ${RESULT}"
|
||||
|
||||
LINE_COUNT=$(echo "${RESULT}" | python3 -c "
|
||||
import sys, json, base64
|
||||
r = json.load(sys.stdin)
|
||||
lines = base64.b64decode(r['stdout']).decode().strip().split('\n')
|
||||
print(len(lines))
|
||||
" 2>/dev/null)
|
||||
|
||||
[ "${LINE_COUNT}" = "3" ] && \
|
||||
pass "Multi-line output: ${LINE_COUNT} lines" || \
|
||||
fail "Expected 3 lines, got ${LINE_COUNT}"
|
||||
|
||||
# ──────────────────────────────────────────────────
|
||||
# Test 6: Pause the sandbox
|
||||
# ──────────────────────────────────────────────────
|
||||
info "Test 6: Pause sandbox"
|
||||
|
||||
RESULT=$(rpc "PauseSandbox" "{\"sandboxId\": \"${SANDBOX_ID}\"}")
|
||||
echo " Response: ${RESULT}"
|
||||
|
||||
# Verify status is paused.
|
||||
LIST=$(rpc "ListSandboxes" '{}')
|
||||
echo "${LIST}" | grep -q '"paused"' && \
|
||||
pass "Sandbox paused" || \
|
||||
fail "Sandbox not in paused state"
|
||||
|
||||
# ──────────────────────────────────────────────────
|
||||
# Test 7: Exec should fail while paused
|
||||
# ──────────────────────────────────────────────────
|
||||
info "Test 7: Exec while paused (expect error)"
|
||||
|
||||
RESULT=$(rpc "Exec" "{
|
||||
\"sandboxId\": \"${SANDBOX_ID}\",
|
||||
\"cmd\": \"/bin/echo\",
|
||||
\"args\": [\"should fail\"]
|
||||
}")
|
||||
echo " Response: ${RESULT}"
|
||||
|
||||
echo "${RESULT}" | grep -qi "not running\|error\|code" && \
|
||||
pass "Exec correctly rejected while paused" || \
|
||||
fail "Exec should have failed while paused"
|
||||
|
||||
# ──────────────────────────────────────────────────
|
||||
# Test 8: Resume the sandbox
|
||||
# ──────────────────────────────────────────────────
|
||||
info "Test 8: Resume sandbox"
|
||||
|
||||
RESULT=$(rpc "ResumeSandbox" "{\"sandboxId\": \"${SANDBOX_ID}\"}")
|
||||
echo " Response: ${RESULT}"
|
||||
|
||||
# Verify status is running.
|
||||
LIST=$(rpc "ListSandboxes" '{}')
|
||||
echo "${LIST}" | grep -q '"running"' && \
|
||||
pass "Sandbox resumed" || \
|
||||
fail "Sandbox not in running state"
|
||||
|
||||
# ──────────────────────────────────────────────────
|
||||
# Test 9: Exec after resume
|
||||
# ──────────────────────────────────────────────────
|
||||
info "Test 9: Exec after resume"
|
||||
|
||||
RESULT=$(rpc "Exec" "{
|
||||
\"sandboxId\": \"${SANDBOX_ID}\",
|
||||
\"cmd\": \"/bin/sh\",
|
||||
\"args\": [\"-c\", \"echo resumed ok\"],
|
||||
\"timeoutSec\": 10
|
||||
}")
|
||||
echo " Response: ${RESULT}"
|
||||
|
||||
STDOUT=$(echo "${RESULT}" | python3 -c "
|
||||
import sys, json, base64
|
||||
r = json.load(sys.stdin)
|
||||
print(base64.b64decode(r['stdout']).decode().strip())
|
||||
" 2>/dev/null) || fail "Exec after resume failed"
|
||||
|
||||
[ "${STDOUT}" = "resumed ok" ] && \
|
||||
pass "Exec after resume works: '${STDOUT}'" || \
|
||||
fail "Expected 'resumed ok', got '${STDOUT}'"
|
||||
|
||||
# ──────────────────────────────────────────────────
|
||||
# Test 10: Destroy the sandbox
|
||||
# ──────────────────────────────────────────────────
|
||||
info "Test 10: Destroy sandbox"
|
||||
|
||||
RESULT=$(rpc "DestroySandbox" "{\"sandboxId\": \"${SANDBOX_ID}\"}")
|
||||
echo " Response: ${RESULT}"
|
||||
pass "Sandbox destroyed"
|
||||
|
||||
# ──────────────────────────────────────────────────
|
||||
# Test 11: List sandboxes (should be empty again)
|
||||
# ──────────────────────────────────────────────────
|
||||
info "Test 11: List sandboxes (expect empty)"
|
||||
|
||||
RESULT=$(rpc "ListSandboxes" '{}')
|
||||
echo " Response: ${RESULT}"
|
||||
|
||||
echo "${RESULT}" | grep -q "${SANDBOX_ID}" && \
|
||||
fail "Destroyed sandbox still in list" || \
|
||||
pass "Sandbox list is clean"
|
||||
|
||||
# ──────────────────────────────────────────────────
|
||||
# Test 12: Destroy non-existent sandbox (expect error)
|
||||
# ──────────────────────────────────────────────────
|
||||
info "Test 12: Destroy non-existent sandbox (expect error)"
|
||||
|
||||
RESULT=$(rpc "DestroySandbox" '{"sandboxId": "sb-nonexist"}')
|
||||
echo " Response: ${RESULT}"
|
||||
|
||||
echo "${RESULT}" | grep -qi "not found\|error\|code" && \
|
||||
pass "Correctly rejected non-existent sandbox" || \
|
||||
fail "Should have returned error for non-existent sandbox"
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}All tests passed!${NC}"
|
||||
73
scripts/update-debug-rootfs.sh
Executable file
73
scripts/update-debug-rootfs.sh
Executable file
@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# update-debug-rootfs.sh — Build envd and inject it (plus wrenn-init) into the debug rootfs.
|
||||
#
|
||||
# This script:
|
||||
# 1. Builds a fresh envd static binary via make
|
||||
# 2. Mounts the rootfs image
|
||||
# 3. Copies envd and wrenn-init into the image
|
||||
# 4. Unmounts cleanly
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/update-debug-rootfs.sh [rootfs_path]
|
||||
#
|
||||
# Defaults to /var/lib/wrenn/images/minimal.ext4
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
ROOTFS="${1:-/var/lib/wrenn/images/minimal.ext4}"
|
||||
MOUNT_DIR="/tmp/wrenn-rootfs-update"
|
||||
|
||||
if [ ! -f "${ROOTFS}" ]; then
|
||||
echo "ERROR: Rootfs not found at ${ROOTFS}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 1: Build envd.
|
||||
echo "==> Building envd..."
|
||||
cd "${PROJECT_ROOT}"
|
||||
make build-envd
|
||||
ENVD_BIN="${PROJECT_ROOT}/builds/envd"
|
||||
|
||||
if [ ! -f "${ENVD_BIN}" ]; then
|
||||
echo "ERROR: envd binary not found at ${ENVD_BIN}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify it's statically linked.
|
||||
if ! file "${ENVD_BIN}" | grep -q "statically linked"; then
|
||||
echo "ERROR: envd is not statically linked!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 2: Mount the rootfs.
|
||||
echo "==> Mounting rootfs at ${MOUNT_DIR}..."
|
||||
mkdir -p "${MOUNT_DIR}"
|
||||
sudo mount -o loop "${ROOTFS}" "${MOUNT_DIR}"
|
||||
|
||||
cleanup() {
|
||||
echo "==> Unmounting rootfs..."
|
||||
sudo umount "${MOUNT_DIR}" 2>/dev/null || true
|
||||
rmdir "${MOUNT_DIR}" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# Step 3: Copy files into rootfs.
|
||||
echo "==> Installing envd..."
|
||||
sudo mkdir -p "${MOUNT_DIR}/usr/local/bin"
|
||||
sudo cp "${ENVD_BIN}" "${MOUNT_DIR}/usr/local/bin/envd"
|
||||
sudo chmod 755 "${MOUNT_DIR}/usr/local/bin/envd"
|
||||
|
||||
echo "==> Installing wrenn-init..."
|
||||
sudo cp "${PROJECT_ROOT}/images/wrenn-init.sh" "${MOUNT_DIR}/usr/local/bin/wrenn-init"
|
||||
sudo chmod 755 "${MOUNT_DIR}/usr/local/bin/wrenn-init"
|
||||
|
||||
# Step 4: Verify.
|
||||
echo ""
|
||||
echo "==> Installed files:"
|
||||
ls -la "${MOUNT_DIR}/usr/local/bin/envd" "${MOUNT_DIR}/usr/local/bin/wrenn-init"
|
||||
|
||||
echo ""
|
||||
echo "==> Done. Rootfs updated: ${ROOTFS}"
|
||||
Reference in New Issue
Block a user