From 17a2aaded39803161245608b3e32e7a153230158 Mon Sep 17 00:00:00 2001 From: Maksym Pavlenko Date: Wed, 24 Nov 2021 10:58:48 -0800 Subject: [PATCH] [sandbox] Add ctr support Signed-off-by: Maksym Pavlenko --- cmd/ctr/app/main.go | 2 + cmd/ctr/commands/sandboxes/sandboxes.go | 196 ++++++++++++++++++++++++ metadata/sandbox.go | 3 +- runtime/v2/bundle.go | 21 ++- runtime/v2/bundle_linux_test.go | 5 +- runtime/v2/manager.go | 2 +- sandbox.go | 33 +++- sandbox/helpers.go | 1 + 8 files changed, 249 insertions(+), 14 deletions(-) create mode 100644 cmd/ctr/commands/sandboxes/sandboxes.go diff --git a/cmd/ctr/app/main.go b/cmd/ctr/app/main.go index 4beb24ba1..f2a352f1a 100644 --- a/cmd/ctr/app/main.go +++ b/cmd/ctr/app/main.go @@ -31,6 +31,7 @@ import ( "github.com/containerd/containerd/cmd/ctr/commands/plugins" "github.com/containerd/containerd/cmd/ctr/commands/pprof" "github.com/containerd/containerd/cmd/ctr/commands/run" + "github.com/containerd/containerd/cmd/ctr/commands/sandboxes" "github.com/containerd/containerd/cmd/ctr/commands/snapshots" "github.com/containerd/containerd/cmd/ctr/commands/tasks" versionCmd "github.com/containerd/containerd/cmd/ctr/commands/version" @@ -114,6 +115,7 @@ containerd CLI tasks.Command, install.Command, ociCmd.Command, + sandboxes.Command, }, extraCmds...) app.Before = func(context *cli.Context) error { if context.GlobalBool("debug") { diff --git a/cmd/ctr/commands/sandboxes/sandboxes.go b/cmd/ctr/commands/sandboxes/sandboxes.go new file mode 100644 index 000000000..73156f660 --- /dev/null +++ b/cmd/ctr/commands/sandboxes/sandboxes.go @@ -0,0 +1,196 @@ +/* + Copyright The containerd 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 sandboxes + +import ( + "fmt" + "os" + "text/tabwriter" + + "github.com/containerd/containerd" + "github.com/containerd/containerd/cmd/ctr/commands" + "github.com/containerd/containerd/defaults" + "github.com/containerd/containerd/log" + "github.com/containerd/containerd/oci" + "github.com/urfave/cli" +) + +// Command is a set of subcommands to manage runtimes with sandbox support +var Command = cli.Command{ + Name: "sandboxes", + Aliases: []string{"sandbox", "sb", "s"}, + Usage: "manage sandboxes", + Subcommands: cli.Commands{ + runCommand, + listCommand, + removeCommand, + pingCommand, + }, +} + +var runCommand = cli.Command{ + Name: "run", + Aliases: []string{"create", "c", "r"}, + Usage: "run a new sandbox", + ArgsUsage: "[flags] ", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "runtime", + Usage: "runtime name", + Value: defaults.DefaultRuntime, + }, + }, + Action: func(context *cli.Context) error { + var ( + id = context.Args().Get(0) + runtime = context.String("runtime") + ) + + client, ctx, cancel, err := commands.NewClient(context) + if err != nil { + return err + } + defer cancel() + + sandbox, err := client.NewSandbox(ctx, id, + containerd.WithSandboxRuntime(runtime, nil), + containerd.WithSandboxSpec(&oci.Spec{}), + ) + if err != nil { + return fmt.Errorf("failed to create new sandbox: %w", err) + } + + err = sandbox.Start(ctx) + if err != nil { + return fmt.Errorf("failed to start: %w", err) + } + + fmt.Println(sandbox.ID()) + return nil + }, +} + +var listCommand = cli.Command{ + Name: "list", + Aliases: []string{"ls"}, + Usage: "list sandboxes", + Flags: []cli.Flag{ + cli.StringSliceFlag{ + Name: "filters", + Usage: "the list of filters to apply when querying sandboxes from the store", + }, + }, + Action: func(context *cli.Context) error { + client, ctx, cancel, err := commands.NewClient(context) + if err != nil { + return err + } + defer cancel() + + var ( + writer = tabwriter.NewWriter(os.Stdout, 1, 8, 1, ' ', 0) + filters = context.StringSlice("filters") + ) + + defer func() { + _ = writer.Flush() + }() + + list, err := client.SandboxStore().List(ctx, filters...) + if err != nil { + return fmt.Errorf("failed to list sandboxes: %w", err) + } + + if _, err := fmt.Fprintln(writer, "ID\tCREATED\tRUNTIME\t"); err != nil { + return err + } + + for _, sandbox := range list { + _, err := fmt.Fprintf(writer, "%s\t%s\t%s\t\n", sandbox.ID, sandbox.CreatedAt, sandbox.Runtime.Name) + if err != nil { + return err + } + } + + return nil + }, +} + +var removeCommand = cli.Command{ + Name: "remove", + Aliases: []string{"rm"}, + ArgsUsage: " [, ...]", + Usage: "remove sandboxes", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "force, f", + Usage: "ignore shutdown errors when removing sandbox", + }, + }, + Action: func(context *cli.Context) error { + client, ctx, cancel, err := commands.NewClient(context) + if err != nil { + return err + } + defer cancel() + + for _, id := range context.Args() { + sandbox, err := client.LoadSandbox(ctx, id) + if err != nil { + log.G(ctx).WithError(err).Errorf("failed to load sandbox %s", id) + continue + } + + err = sandbox.Shutdown(ctx, context.Bool("force")) + if err != nil { + log.G(ctx).WithError(err).Errorf("failed to shutdown sandbox %s", id) + continue + } + + log.G(ctx).Infof("deleted: %s", id) + } + + return nil + }, +} + +var pingCommand = cli.Command{ + Name: "ping", + ArgsUsage: " [, ...]", + Usage: "ping sandbox", + Action: func(context *cli.Context) error { + client, ctx, cancel, err := commands.NewClient(context) + if err != nil { + return err + } + defer cancel() + + for _, id := range context.Args() { + sandbox, err := client.LoadSandbox(ctx, id) + if err != nil { + return fmt.Errorf("failed to load sandbox %s: %w", id, err) + } + + err = sandbox.Ping(ctx) + if err != nil { + return fmt.Errorf("failed to ping %s: %w", id, err) + } + } + + return nil + }, +} diff --git a/metadata/sandbox.go b/metadata/sandbox.go index ecff782cd..bcdfce8a3 100644 --- a/metadata/sandbox.go +++ b/metadata/sandbox.go @@ -202,7 +202,8 @@ func (s *sandboxStore) List(ctx context.Context, fields ...string) ([]api.Sandbo if err := view(ctx, s.db, func(tx *bbolt.Tx) error { bucket := getSandboxBucket(tx, ns) if bucket == nil { - return errors.Wrap(errdefs.ErrNotFound, "no sandbox buckets") + // We haven't created any sandboxes yet, just return empty list + return nil } if err := bucket.ForEach(func(k, v []byte) error { diff --git a/runtime/v2/bundle.go b/runtime/v2/bundle.go index 8152a5277..8282d540b 100644 --- a/runtime/v2/bundle.go +++ b/runtime/v2/bundle.go @@ -25,6 +25,8 @@ import ( "github.com/containerd/containerd/identifiers" "github.com/containerd/containerd/mount" "github.com/containerd/containerd/namespaces" + "github.com/containerd/typeurl" + "github.com/opencontainers/runtime-spec/specs-go" ) const configFilename = "config.json" @@ -43,7 +45,7 @@ func LoadBundle(ctx context.Context, root, id string) (*Bundle, error) { } // NewBundle returns a new bundle on disk -func NewBundle(ctx context.Context, root, state, id string, spec []byte) (b *Bundle, err error) { +func NewBundle(ctx context.Context, root, state, id string, spec typeurl.Any) (b *Bundle, err error) { if err := identifiers.Validate(id); err != nil { return nil, fmt.Errorf("invalid task id %s: %w", id, err) } @@ -73,8 +75,10 @@ func NewBundle(ctx context.Context, root, state, id string, spec []byte) (b *Bun if err := os.Mkdir(b.Path, 0700); err != nil { return nil, err } - if err := prepareBundleDirectoryPermissions(b.Path, spec); err != nil { - return nil, err + if typeurl.Is(spec, &specs.Spec{}) { + if err := prepareBundleDirectoryPermissions(b.Path, spec.GetValue()); err != nil { + return nil, err + } } paths = append(paths, b.Path) // create working directory for the bundle @@ -100,9 +104,14 @@ func NewBundle(ctx context.Context, root, state, id string, spec []byte) (b *Bun if err := os.Symlink(work, filepath.Join(b.Path, "work")); err != nil { return nil, err } - // write the spec to the bundle - err = os.WriteFile(filepath.Join(b.Path, configFilename), spec, 0666) - return b, err + if spec := spec.GetValue(); spec != nil { + // write the spec to the bundle + err = os.WriteFile(filepath.Join(b.Path, configFilename), spec, 0666) + if err != nil { + return nil, fmt.Errorf("failed to write %s", configFilename) + } + } + return b, nil } // Bundle represents an OCI bundle diff --git a/runtime/v2/bundle_linux_test.go b/runtime/v2/bundle_linux_test.go index 685dc2fbf..f34859453 100644 --- a/runtime/v2/bundle_linux_test.go +++ b/runtime/v2/bundle_linux_test.go @@ -29,6 +29,7 @@ import ( "github.com/containerd/containerd/namespaces" "github.com/containerd/containerd/oci" "github.com/containerd/containerd/pkg/testutil" + "github.com/containerd/typeurl" "github.com/opencontainers/runtime-spec/specs-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -57,11 +58,11 @@ func TestNewBundle(t *testing.T) { GIDMappings: []specs.LinuxIDMapping{{ContainerID: 0, HostID: usernsGID}}, } } - specBytes, err := json.Marshal(&spec) + specAny, err := typeurl.MarshalAny(&spec) require.NoError(t, err, "failed to marshal spec") ctx := namespaces.WithNamespace(context.TODO(), namespaces.Default) - b, err := NewBundle(ctx, work, state, id, specBytes) + b, err := NewBundle(ctx, work, state, id, specAny) require.NoError(t, err, "NewBundle should succeed") require.NotNil(t, b, "bundle should not be nil") diff --git a/runtime/v2/manager.go b/runtime/v2/manager.go index 51c6df616..b7bd9bf95 100644 --- a/runtime/v2/manager.go +++ b/runtime/v2/manager.go @@ -158,7 +158,7 @@ func (m *ShimManager) ID() string { // Start launches a new shim instance func (m *ShimManager) Start(ctx context.Context, id string, opts runtime.CreateOpts) (_ ShimProcess, retErr error) { - bundle, err := NewBundle(ctx, m.root, m.state, id, opts.Spec.GetValue()) + bundle, err := NewBundle(ctx, m.root, m.state, id, opts.Spec) if err != nil { return nil, err } diff --git a/sandbox.go b/sandbox.go index 6c30906f5..805002aff 100644 --- a/sandbox.go +++ b/sandbox.go @@ -18,6 +18,7 @@ package containerd import ( "context" + "fmt" "time" api "github.com/containerd/containerd/sandbox" @@ -36,8 +37,9 @@ type Sandbox interface { Labels(ctx context.Context) (map[string]string, error) // Start starts new sandbox instance Start(ctx context.Context) error - // Shutdown will turn down existing sandbox instance - Shutdown(ctx context.Context) error + // Shutdown will turn down existing sandbox instance. + // If using force, the client will ignore shutdown errors. + Shutdown(ctx context.Context, force bool) error // Pause will freeze running sandbox instance Pause(ctx context.Context) error // Resume will unfreeze previously paused sandbox instance @@ -74,8 +76,23 @@ func (s *sandboxClient) Start(ctx context.Context) error { return s.client.SandboxController().Start(ctx, s.ID()) } -func (s *sandboxClient) Shutdown(ctx context.Context) error { - return s.client.SandboxController().Shutdown(ctx, s.ID()) +func (s *sandboxClient) Shutdown(ctx context.Context, force bool) error { + var ( + controller = s.client.SandboxController() + store = s.client.SandboxStore() + ) + + err := controller.Shutdown(ctx, s.ID()) + if err != nil && !force { + return fmt.Errorf("failed to shutdown sandbox: %w", err) + } + + err = store.Delete(ctx, s.ID()) + if err != nil { + return fmt.Errorf("failed to delete sandbox from metadata store: %w", err) + } + + return nil } func (s *sandboxClient) Pause(ctx context.Context) error { @@ -105,6 +122,10 @@ func (s *sandboxClient) Status(ctx context.Context, status interface{}) error { // NewSandbox creates new sandbox client func (c *Client) NewSandbox(ctx context.Context, sandboxID string, opts ...NewSandboxOpts) (Sandbox, error) { + if sandboxID == "" { + return nil, errors.New("sandbox ID must be specified") + } + newSandbox := api.Sandbox{ ID: sandboxID, CreatedAt: time.Now().UTC(), @@ -147,6 +168,10 @@ type NewSandboxOpts func(ctx context.Context, client *Client, sandbox *api.Sandb // WithSandboxRuntime allows a user to specify the runtime to be used to run a sandbox func WithSandboxRuntime(name string, options interface{}) NewSandboxOpts { return func(ctx context.Context, client *Client, s *api.Sandbox) error { + if options == nil { + options = &types.Empty{} + } + opts, err := typeurl.MarshalAny(options) if err != nil { return errors.Wrap(err, "failed to marshal sandbox runtime options") diff --git a/sandbox/helpers.go b/sandbox/helpers.go index 9f155bf07..ad984e9e8 100644 --- a/sandbox/helpers.go +++ b/sandbox/helpers.go @@ -47,6 +47,7 @@ func FromProto(p *types.Sandbox) Sandbox { ID: p.SandboxID, Labels: p.Labels, Runtime: runtime, + Spec: p.Spec, CreatedAt: p.CreatedAt, UpdatedAt: p.UpdatedAt, Extensions: p.Extensions,