From eaccbf1d03428585d46f8cb5f128564e937e1127 Mon Sep 17 00:00:00 2001 From: Maksym Pavlenko Date: Mon, 19 Apr 2021 15:19:29 -0700 Subject: [PATCH] [sandbox] Add clients Signed-off-by: Maksym Pavlenko --- client.go | 24 ++++- cmd/containerd/builtins.go | 1 + container_opts.go | 9 ++ sandbox.go | 198 +++++++++++++++++++++++++++++++++++++ sandbox_controller.go | 92 +++++++++++++++++ sandbox_store.go | 95 ++++++++++++++++++ services.go | 18 ++++ 7 files changed, 436 insertions(+), 1 deletion(-) create mode 100644 sandbox.go create mode 100644 sandbox_controller.go create mode 100644 sandbox_store.go diff --git a/client.go b/client.go index 1c2202e1e..defcc35f7 100644 --- a/client.go +++ b/client.go @@ -35,6 +35,7 @@ import ( introspectionapi "github.com/containerd/containerd/api/services/introspection/v1" leasesapi "github.com/containerd/containerd/api/services/leases/v1" namespacesapi "github.com/containerd/containerd/api/services/namespaces/v1" + sandboxsapi "github.com/containerd/containerd/api/services/sandbox/v1" snapshotsapi "github.com/containerd/containerd/api/services/snapshots/v1" "github.com/containerd/containerd/api/services/tasks/v1" versionservice "github.com/containerd/containerd/api/services/version/v1" @@ -54,13 +55,14 @@ import ( "github.com/containerd/containerd/plugin" "github.com/containerd/containerd/remotes" "github.com/containerd/containerd/remotes/docker" + "github.com/containerd/containerd/sandbox" "github.com/containerd/containerd/services/introspection" "github.com/containerd/containerd/snapshots" snproxy "github.com/containerd/containerd/snapshots/proxy" "github.com/containerd/typeurl" ptypes "github.com/gogo/protobuf/types" ocispec "github.com/opencontainers/image-spec/specs-go/v1" - specs "github.com/opencontainers/runtime-spec/specs-go" + "github.com/opencontainers/runtime-spec/specs-go" "golang.org/x/sync/semaphore" "google.golang.org/grpc" "google.golang.org/grpc/backoff" @@ -688,6 +690,26 @@ func (c *Client) EventService() EventService { return NewEventServiceFromClient(eventsapi.NewEventsClient(c.conn)) } +// SandboxStore returns the underlying sandbox store client +func (c *Client) SandboxStore() sandbox.Store { + if c.sandboxStore != nil { + return c.sandboxStore + } + c.connMu.Lock() + defer c.connMu.Unlock() + return NewRemoteSandboxStore(sandboxsapi.NewStoreClient(c.conn)) +} + +// SandboxController returns the underlying sandbox controller client +func (c *Client) SandboxController() sandbox.Controller { + if c.sandboxController != nil { + return c.sandboxController + } + c.connMu.Lock() + defer c.connMu.Unlock() + return NewSandboxRemoteController(sandboxsapi.NewControllerClient(c.conn)) +} + // VersionService returns the underlying VersionClient func (c *Client) VersionService() versionservice.VersionClient { c.connMu.Lock() diff --git a/cmd/containerd/builtins.go b/cmd/containerd/builtins.go index dd8b1d3d9..792adbe3c 100644 --- a/cmd/containerd/builtins.go +++ b/cmd/containerd/builtins.go @@ -33,6 +33,7 @@ import ( _ "github.com/containerd/containerd/services/leases" _ "github.com/containerd/containerd/services/namespaces" _ "github.com/containerd/containerd/services/opt" + _ "github.com/containerd/containerd/services/sandbox" _ "github.com/containerd/containerd/services/snapshots" _ "github.com/containerd/containerd/services/tasks" _ "github.com/containerd/containerd/services/version" diff --git a/container_opts.go b/container_opts.go index f005fe1c7..76a9b9616 100644 --- a/container_opts.go +++ b/container_opts.go @@ -74,6 +74,15 @@ func WithRuntime(name string, options interface{}) NewContainerOpts { } } +// WithSandbox joins the container to a container group (aka sandbox) from the given ID +// Note: shim runtime must support sandboxes environments. +func WithSandbox(sandboxID string) NewContainerOpts { + return func(ctx context.Context, client *Client, c *containers.Container) error { + c.SandboxID = sandboxID + return nil + } +} + // WithImage sets the provided image as the base for the container func WithImage(i Image) NewContainerOpts { return func(ctx context.Context, client *Client, c *containers.Container) error { diff --git a/sandbox.go b/sandbox.go new file mode 100644 index 000000000..9449bdabb --- /dev/null +++ b/sandbox.go @@ -0,0 +1,198 @@ +/* + 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 containerd + +import ( + "context" + "time" + + api "github.com/containerd/containerd/sandbox" + "github.com/containerd/typeurl" + "github.com/gogo/protobuf/types" + "github.com/pkg/errors" +) + +// Sandbox is a high level client to containerd's sandboxes. +type Sandbox interface { + // ID is a sandbox identifier + ID() string + // NewContainer creates new container that will belong to this sandbox + NewContainer(ctx context.Context, id string, opts ...NewContainerOpts) (Container, error) + // Labels returns the labels set on the sandbox + 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 + // Pause will freeze running sandbox instance + Pause(ctx context.Context) error + // Resume will unfreeze previously paused sandbox instance + Resume(ctx context.Context) error + // Status will return current sandbox status (provided by shim runtime) + Status(ctx context.Context, status interface{}) error +} + +type sandboxClient struct { + client *Client + metadata api.Sandbox +} + +func (s *sandboxClient) ID() string { + return s.metadata.ID +} + +func (s *sandboxClient) NewContainer(ctx context.Context, id string, opts ...NewContainerOpts) (Container, error) { + return s.client.NewContainer(ctx, id, append(opts, WithSandbox(s.ID()))...) +} + +func (s *sandboxClient) Labels(ctx context.Context) (map[string]string, error) { + sandbox, err := s.client.SandboxStore().Get(ctx, s.ID()) + if err != nil { + return nil, err + } + + return sandbox.Labels, nil +} + +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) Pause(ctx context.Context) error { + return s.client.SandboxController().Pause(ctx, s.ID()) +} + +func (s *sandboxClient) Resume(ctx context.Context) error { + return s.client.SandboxController().Resume(ctx, s.ID()) +} + +func (s *sandboxClient) Ping(ctx context.Context) error { + return s.client.SandboxController().Ping(ctx, s.ID()) +} + +func (s *sandboxClient) Status(ctx context.Context, status interface{}) error { + any, err := s.client.SandboxController().Status(ctx, s.ID()) + if err != nil { + return err + } + + if err := typeurl.UnmarshalTo(any, status); err != nil { + return errors.Wrap(err, "failed to unmarshal sandbox status") + } + + return nil +} + +// NewSandbox creates new sandbox client +func (c *Client) NewSandbox(ctx context.Context, sandboxID string, opts ...NewSandboxOpts) (Sandbox, error) { + newSandbox := api.Sandbox{ + ID: sandboxID, + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), + } + + for _, opt := range opts { + if err := opt(ctx, c, &newSandbox); err != nil { + return nil, err + } + } + + metadata, err := c.SandboxStore().Create(ctx, newSandbox) + if err != nil { + return nil, err + } + + return &sandboxClient{ + client: c, + metadata: metadata, + }, nil +} + +// LoadSandbox laods existing sandbox metadata object using the id +func (c *Client) LoadSandbox(ctx context.Context, id string) (Sandbox, error) { + sandbox, err := c.SandboxStore().Get(ctx, id) + if err != nil { + return nil, err + } + + return &sandboxClient{ + client: c, + metadata: sandbox, + }, nil +} + +// NewSandboxOpts is a sandbox options and extensions to be provided by client +type NewSandboxOpts func(ctx context.Context, client *Client, sandbox *api.Sandbox) error + +// 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 { + opts, err := typeurl.MarshalAny(options) + if err != nil { + return errors.Wrap(err, "failed to marshal sandbox runtime options") + } + + s.Runtime = api.RuntimeOpts{ + Name: name, + Options: opts, + } + + return nil + } +} + +// WithSandboxSpec will provide the sandbox runtime spec +func WithSandboxSpec(spec interface{}) NewSandboxOpts { + return func(ctx context.Context, client *Client, sandbox *api.Sandbox) error { + spec, err := typeurl.MarshalAny(spec) + if err != nil { + return errors.Wrap(err, "failed to marshal spec") + } + + sandbox.Spec = spec + return nil + } +} + +// WithSandboxExtension attaches an extension to sandbox +func WithSandboxExtension(name string, ext interface{}) NewSandboxOpts { + return func(ctx context.Context, client *Client, s *api.Sandbox) error { + if s.Extensions == nil { + s.Extensions = make(map[string]types.Any) + } + + any, err := typeurl.MarshalAny(ext) + if err != nil { + return errors.Wrap(err, "failed to marshal sandbox extension") + } + + s.Extensions[name] = *any + return err + } +} + +// WithSandboxLabels attaches map of labels to sandbox +func WithSandboxLabels(labels map[string]string) NewSandboxOpts { + return func(ctx context.Context, client *Client, sandbox *api.Sandbox) error { + sandbox.Labels = labels + return nil + } +} diff --git a/sandbox_controller.go b/sandbox_controller.go new file mode 100644 index 000000000..ecb7e093d --- /dev/null +++ b/sandbox_controller.go @@ -0,0 +1,92 @@ +/* + 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 containerd + +import ( + "context" + + api "github.com/containerd/containerd/api/services/sandbox/v1" + "github.com/containerd/containerd/errdefs" + sb "github.com/containerd/containerd/sandbox" + "github.com/gogo/protobuf/types" +) + +// sandboxRemoteController is a low level GRPC client for containerd's sandbox controller service +type sandboxRemoteController struct { + client api.ControllerClient +} + +var _ sb.Controller = (*sandboxRemoteController)(nil) + +// NewSandboxRemoteController creates client for sandbox controller +func NewSandboxRemoteController(client api.ControllerClient) sb.Controller { + return &sandboxRemoteController{client: client} +} + +func (s *sandboxRemoteController) Start(ctx context.Context, sandboxID string) error { + if _, err := s.client.Start(ctx, &api.ControllerStartRequest{ + SandboxID: sandboxID, + }); err != nil { + return errdefs.FromGRPC(err) + } + + return nil +} + +func (s *sandboxRemoteController) Shutdown(ctx context.Context, sandboxID string) error { + _, err := s.client.Shutdown(ctx, &api.ControllerShutdownRequest{SandboxID: sandboxID}) + if err != nil { + return errdefs.FromGRPC(err) + } + + return nil +} + +func (s *sandboxRemoteController) Pause(ctx context.Context, sandboxID string) error { + _, err := s.client.Pause(ctx, &api.ControllerPauseRequest{SandboxID: sandboxID}) + if err != nil { + return errdefs.FromGRPC(err) + } + + return nil +} + +func (s *sandboxRemoteController) Resume(ctx context.Context, sandboxID string) error { + _, err := s.client.Resume(ctx, &api.ControllerResumeRequest{SandboxID: sandboxID}) + if err != nil { + return errdefs.FromGRPC(err) + } + + return nil +} + +func (s *sandboxRemoteController) Ping(ctx context.Context, sandboxID string) error { + if _, err := s.client.Ping(ctx, &api.ControllerPingRequest{SandboxID: sandboxID}); err != nil { + return errdefs.FromGRPC(err) + } + + return nil +} + +func (s *sandboxRemoteController) Status(ctx context.Context, sandboxID string) (*types.Any, error) { + resp, err := s.client.Status(ctx, &api.ControllerStatusRequest{SandboxID: sandboxID}) + if err != nil { + return nil, errdefs.FromGRPC(err) + } + + return resp.Status, nil +} diff --git a/sandbox_store.go b/sandbox_store.go new file mode 100644 index 000000000..3fe437353 --- /dev/null +++ b/sandbox_store.go @@ -0,0 +1,95 @@ +/* + 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 containerd + +import ( + "context" + + api "github.com/containerd/containerd/api/services/sandbox/v1" + "github.com/containerd/containerd/errdefs" + sb "github.com/containerd/containerd/sandbox" +) + +// remoteSandboxStore is a low-level containerd client to manage sandbox environments metadata +type remoteSandboxStore struct { + client api.StoreClient +} + +var _ sb.Store = (*remoteSandboxStore)(nil) + +// NewRemoteSandboxStore create client for sandbox store +func NewRemoteSandboxStore(client api.StoreClient) sb.Store { + return &remoteSandboxStore{client: client} +} + +func (s *remoteSandboxStore) Create(ctx context.Context, sandbox sb.Sandbox) (sb.Sandbox, error) { + resp, err := s.client.Create(ctx, &api.StoreCreateRequest{ + Sandbox: sb.ToProto(&sandbox), + }) + if err != nil { + return sb.Sandbox{}, errdefs.FromGRPC(err) + } + + return sb.FromProto(&resp.Sandbox), nil +} + +func (s *remoteSandboxStore) Update(ctx context.Context, sandbox sb.Sandbox, fieldpaths ...string) (sb.Sandbox, error) { + resp, err := s.client.Update(ctx, &api.StoreUpdateRequest{ + Sandbox: sb.ToProto(&sandbox), + Fields: fieldpaths, + }) + if err != nil { + return sb.Sandbox{}, errdefs.FromGRPC(err) + } + + return sb.FromProto(&resp.Sandbox), nil +} + +func (s *remoteSandboxStore) Get(ctx context.Context, id string) (sb.Sandbox, error) { + resp, err := s.client.Get(ctx, &api.StoreGetRequest{ + SandboxID: id, + }) + if err != nil { + return sb.Sandbox{}, errdefs.FromGRPC(err) + } + + return sb.FromProto(resp.Sandbox), nil +} + +func (s *remoteSandboxStore) List(ctx context.Context, filters ...string) ([]sb.Sandbox, error) { + resp, err := s.client.List(ctx, &api.StoreListRequest{ + Filters: filters, + }) + if err != nil { + return nil, errdefs.FromGRPC(err) + } + + out := make([]sb.Sandbox, len(resp.List)) + for i, s := range resp.List { + out[i] = sb.FromProto(&s) + } + + return out, nil +} + +func (s *remoteSandboxStore) Delete(ctx context.Context, id string) error { + _, err := s.client.Delete(ctx, &api.StoreDeleteRequest{ + SandboxID: id, + }) + + return errdefs.FromGRPC(err) +} diff --git a/services.go b/services.go index e780e6ccf..3e1c66835 100644 --- a/services.go +++ b/services.go @@ -22,12 +22,14 @@ import ( imagesapi "github.com/containerd/containerd/api/services/images/v1" introspectionapi "github.com/containerd/containerd/api/services/introspection/v1" namespacesapi "github.com/containerd/containerd/api/services/namespaces/v1" + sandboxsapi "github.com/containerd/containerd/api/services/sandbox/v1" "github.com/containerd/containerd/api/services/tasks/v1" "github.com/containerd/containerd/containers" "github.com/containerd/containerd/content" "github.com/containerd/containerd/images" "github.com/containerd/containerd/leases" "github.com/containerd/containerd/namespaces" + "github.com/containerd/containerd/sandbox" "github.com/containerd/containerd/services/introspection" "github.com/containerd/containerd/snapshots" ) @@ -43,6 +45,8 @@ type services struct { eventService EventService leasesService leases.Manager introspectionService introspection.Service + sandboxStore sandbox.Store + sandboxController sandbox.Controller } // ServicesOpt allows callers to set options on the services @@ -155,3 +159,17 @@ func WithIntrospectionService(in introspection.Service) ServicesOpt { s.introspectionService = in } } + +// WithSandboxStore sets the sandbox store. +func WithSandboxStore(client sandboxsapi.StoreClient) ServicesOpt { + return func(s *services) { + s.sandboxStore = NewRemoteSandboxStore(client) + } +} + +// WithSandboxController sets the sandbox controller. +func WithSandboxController(client sandboxsapi.ControllerClient) ServicesOpt { + return func(s *services) { + s.sandboxController = NewSandboxRemoteController(client) + } +}