Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ go_path(
"//tools/checklocks",

# Packages that are not dependencies of the above.
"//runsc/client",
"//pkg/sentry/kernel/memevent",
"//pkg/sentry/socket/plugin/stack",
"//pkg/tcpip/adapters/gonet",
Expand Down
32 changes: 32 additions & 0 deletions runsc/client/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
load("//tools:defs.bzl", "go_library", "go_test")

package(
default_applicable_licenses = ["//:license"],
licenses = ["notice"],
)

go_library(
name = "client",
srcs = [
"client.go",
],
visibility = ["//visibility:public"],
deps = [
"//runsc/config",
"//runsc/container",
"@com_github_opencontainers_runtime_spec//specs-go:go_default_library",
],
)

go_test(
name = "client_test",
size = "small",
srcs = [
"client_test.go",
],
embed = [":client"],
deps = [
"//runsc/config",
"//runsc/container",
],
)
123 changes: 123 additions & 0 deletions runsc/client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright 2026 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package client provides programmatic access to gVisor sandboxes and
// containers without spawning runsc subprocesses.
package client

import (
"context"
"fmt"

specs "github.com/opencontainers/runtime-spec/specs-go"
"gvisor.dev/gvisor/runsc/config"
"gvisor.dev/gvisor/runsc/container"
)

// Client provides programmatic access to containers managed in a runsc root directory.
type Client struct {
conf *config.Config
}

// New creates a new Client configured with the specified Config.
// If conf is nil or has an empty RootDir, config.DefaultRootDir() is used.
func New(conf *config.Config) *Client {
if conf == nil {
conf = &config.Config{}
}
if conf.RootDir == "" {
conf.RootDir = config.DefaultRootDir()
}
return &Client{conf: conf}
}

// NewFromRootDir creates a new Client configured for the specified root directory.
// If rootDir is empty, config.DefaultRootDir() is used.
func NewFromRootDir(rootDir string) *Client {
return New(&config.Config{RootDir: rootDir})
}

// RootDir returns the root directory associated with this client.
func (c *Client) RootDir() string {
return c.conf.RootDir
}

// Config returns the configuration associated with this client.
func (c *Client) Config() *config.Config {
return c.conf
}

// Load loads a container by ID from the root directory.
func (c *Client) Load(ctx context.Context, id string, opts container.LoadOpts) (*container.Container, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
return container.Load(c.conf.RootDir, container.FullID{ContainerID: id}, opts)
}

// List returns the full IDs of all containers in the root directory.
func (c *Client) List(ctx context.Context) ([]container.FullID, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
return container.List(c.conf.RootDir)
}

// State returns the OCI state metadata for the container with the given ID.
func (c *Client) State(ctx context.Context, id string) (specs.State, error) {
if err := ctx.Err(); err != nil {
return specs.State{}, err
}
cont, err := c.Load(ctx, id, container.LoadOpts{ReadOnly: true})
if err != nil {
return specs.State{}, fmt.Errorf("loading container %q: %w", id, err)
}
return cont.State(), nil
}

// Spec returns the OCI specification for the container with the given ID.
func (c *Client) Spec(ctx context.Context, id string) (*specs.Spec, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
cont, err := c.Load(ctx, id, container.LoadOpts{ReadOnly: true})
if err != nil {
return nil, fmt.Errorf("loading container %q: %w", id, err)
}
return cont.Spec, nil
}

// Pause pauses the container with the given ID.
func (c *Client) Pause(ctx context.Context, id string) error {
if err := ctx.Err(); err != nil {
return err
}
cont, err := c.Load(ctx, id, container.LoadOpts{})
if err != nil {
return fmt.Errorf("loading container %q: %w", id, err)
}
return cont.Pause()
}

// Resume resumes the container with the given ID.
func (c *Client) Resume(ctx context.Context, id string) error {
if err := ctx.Err(); err != nil {
return err
}
cont, err := c.Load(ctx, id, container.LoadOpts{})
if err != nil {
return fmt.Errorf("loading container %q: %w", id, err)
}
return cont.Resume()
}
137 changes: 137 additions & 0 deletions runsc/client/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Copyright 2026 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package client

import (
"context"
"os"
"testing"

"gvisor.dev/gvisor/runsc/config"
"gvisor.dev/gvisor/runsc/container"
)

func TestClientNew(t *testing.T) {
// Test New with nil config
cNil := New(nil)
if got, want := cNil.RootDir(), config.DefaultRootDir(); got != want {
t.Errorf("New(nil).RootDir() = %q, want %q", got, want)
}

// Test New with empty RootDir
cEmpty := New(&config.Config{})
if got, want := cEmpty.RootDir(), config.DefaultRootDir(); got != want {
t.Errorf("New(&Config{}).RootDir() = %q, want %q", got, want)
}

// Test New with config
conf := &config.Config{
RootDir: "/tmp/runsc-test",
Debug: true,
}
c := New(conf)
if got := c.RootDir(); got != "/tmp/runsc-test" {
t.Errorf("RootDir() = %q, want %q", got, "/tmp/runsc-test")
}
if got := c.Config(); got != conf {
t.Errorf("Config() = %v, want %v", got, conf)
}
if !c.Config().Debug {
t.Errorf("Config().Debug = false, want true")
}

// Test NewFromRootDir
cRoot := NewFromRootDir("/tmp/custom-root")
if got := cRoot.RootDir(); got != "/tmp/custom-root" {
t.Errorf("NewFromRootDir().RootDir() = %q, want %q", got, "/tmp/custom-root")
}
}

func TestClientLoadNonExistent(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "runsc-client-test-")
if err != nil {
t.Fatalf("MkdirTemp failed: %v", err)
}
defer os.RemoveAll(tmpDir)

c := NewFromRootDir(tmpDir)
_, err = c.Load(context.Background(), "nonexistent-container-id", container.LoadOpts{})
if err == nil {
t.Fatalf("Load() expected error for nonexistent container, got nil")
}
}

func TestClientPauseResumeStateNonExistent(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "runsc-client-test-")
if err != nil {
t.Fatalf("MkdirTemp failed: %v", err)
}
defer os.RemoveAll(tmpDir)

c := NewFromRootDir(tmpDir)
ctx := context.Background()

if err := c.Pause(ctx, "nonexistent"); err == nil {
t.Errorf("Pause() expected error for nonexistent container, got nil")
}
if err := c.Resume(ctx, "nonexistent"); err == nil {
t.Errorf("Resume() expected error for nonexistent container, got nil")
}
if _, err := c.State(ctx, "nonexistent"); err == nil {
t.Errorf("State() expected error for nonexistent container, got nil")
}
if _, err := c.Spec(ctx, "nonexistent"); err == nil {
t.Errorf("Spec() expected error for nonexistent container, got nil")
}
}

func TestClientListEmpty(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "runsc-client-test-")
if err != nil {
t.Fatalf("MkdirTemp failed: %v", err)
}
defer os.RemoveAll(tmpDir)

c := NewFromRootDir(tmpDir)
list, err := c.List(context.Background())
if err != nil {
t.Fatalf("List() unexpected error: %v", err)
}
if len(list) != 0 {
t.Errorf("List() returned %d items, want 0", len(list))
}
}

func TestClientContextCancellation(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "runsc-client-test-")
if err != nil {
t.Fatalf("MkdirTemp failed: %v", err)
}
defer os.RemoveAll(tmpDir)

c := NewFromRootDir(tmpDir)
ctx, cancel := context.WithCancel(context.Background())
cancel()

_, err = c.Load(ctx, "any-id", container.LoadOpts{})
if err != context.Canceled {
t.Errorf("Load() with canceled context: got %v, want %v", err, context.Canceled)
}

_, err = c.List(ctx)
if err != context.Canceled {
t.Errorf("List() with canceled context: got %v, want %v", err, context.Canceled)
}
}
1 change: 1 addition & 0 deletions runsc/cmd/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ go_library(
"//pkg/unet",
"//pkg/urpc",
"//runsc/boot/sentryapi",
"//runsc/client",
"//runsc/cmd/metricserver/metricservercmd",
"//runsc/cmd/nvproxy",
"//runsc/cmd/sandboxsetup",
Expand Down
24 changes: 11 additions & 13 deletions runsc/cmd/pause.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,14 @@ import (

"github.com/google/subcommands"
specs "github.com/opencontainers/runtime-spec/specs-go"
"gvisor.dev/gvisor/runsc/client"
"gvisor.dev/gvisor/runsc/cmd/util"
"gvisor.dev/gvisor/runsc/config"
"gvisor.dev/gvisor/runsc/container"
"gvisor.dev/gvisor/runsc/flag"
)

// Pause implements subcommands.Command for the "pause" command.
type Pause struct {
containerLoader
}

// Name implements subcommands.Command.Name.
Expand All @@ -52,28 +51,27 @@ func (*Pause) SetFlags(*flag.FlagSet) {

// FetchSpec implements util.SubCommand.FetchSpec.
func (p *Pause) FetchSpec(conf *config.Config, f *flag.FlagSet) (string, *specs.Spec, error) {
c, err := p.loadContainer(conf, f, container.LoadOpts{})
if f.NArg() < 1 {
f.Usage()
return "", nil, fmt.Errorf("a container-id is required")
}
id := f.Arg(0)
spec, err := client.New(conf).Spec(context.Background(), id)
if err != nil {
return "", nil, fmt.Errorf("loading container: %w", err)
return "", nil, err
}
return c.ID, c.Spec, nil
return id, spec, nil
}

// Execute implements subcommands.Command.Execute.
func (p *Pause) Execute(_ context.Context, f *flag.FlagSet, args ...any) subcommands.ExitStatus {
func (p *Pause) Execute(ctx context.Context, f *flag.FlagSet, args ...any) subcommands.ExitStatus {
if f.NArg() != 1 {
f.Usage()
return subcommands.ExitUsageError
}

conf := args[0].(*config.Config)

cont, err := p.loadContainer(conf, f, container.LoadOpts{})
if err != nil {
util.Fatalf("loading container: %v", err)
}

if err := cont.Pause(); err != nil {
if err := client.New(conf).Pause(ctx, f.Arg(0)); err != nil {
util.Fatalf("pause failed: %v", err)
}

Expand Down
Loading
Loading