容器处理同样从路由开始追踪,通过路由代码可以看到核心处理组件为 daemon.Daemon 实例。当请求到达 HTTP Server 后,根据匹配的路由处理方法进行预处理(参数是否正确、配置文件格式是否正确等)后,最终由 Backend 实例进行处理。
图 1:Container 路由
container.Backend 接口定义如下,根据不同的方法将接口归类,以 22 ~ 33 行的 stateBackend 为例,它定义了容器状态相关的方法,其他类似。
// execBackend includes functions to implement to provide exec functionality.type execBackend interface {ContainerExecCreate(name string, config *types.ExecConfig) (string, error)ContainerExecInspect(id string) (*backend.ExecInspect, error)ContainerExecResize(name string, height, width int) errorContainerExecStart(ctx context.Context, name string, stdin io.Reader, stdout io.Writer, stderr io.Writer) errorExecExists(name string) (bool, error)}// copyBackend includes functions to implement to provide container copy functionality.type copyBackend interface {ContainerArchivePath(name string, path string) (content io.ReadCloser, stat *types.ContainerPathStat, err error)ContainerCopy(name string, res string) (io.ReadCloser, error)ContainerExport(name string, out io.Writer) errorContainerExtractToDir(name, path string, copyUIDGID, noOverwriteDirNonDir bool, content io.Reader) errorContainerStatPath(name string, path string) (stat *types.ContainerPathStat, err error)}// stateBackend includes functions to implement to provide container state lifecycle functionality.type stateBackend interface {ContainerCreate(config types.ContainerCreateConfig) (container.ContainerCreateCreatedBody, error)ContainerKill(name string, sig uint64) errorContainerPause(name string) errorContainerRename(oldName, newName string) errorContainerResize(name string, height, width int) errorContainerRestart(name string, seconds *int) errorContainerRm(name string, config *types.ContainerRmConfig) errorContainerStart(name string, hostConfig *container.HostConfig, checkpoint string, checkpointDir string) errorContainerStop(name string, seconds *int) errorContainerUnpause(name string) errorContainerUpdate(name string, hostConfig *container.HostConfig) (container.ContainerUpdateOKBody, error)ContainerWait(ctx context.Context, name string, condition containerpkg.WaitCondition) (<-chan containerpkg.StateStatus, error)}// monitorBackend includes functions to implement to provide containers monitoring functionality.type monitorBackend interface {ContainerChanges(name string) ([]archive.Change, error)ContainerInspect(name string, size bool, version string) (interface{}, error)ContainerLogs(ctx context.Context, name string, config *types.ContainerLogsOptions) (msgs <-chan *backend.LogMessage, tty bool, err error)ContainerStats(ctx context.Context, name string, config *backend.ContainerStatsConfig) errorContainerTop(name string, psArgs string) (*container.ContainerTopOKBody, error)Containers(config *types.ContainerListOptions) ([]*types.Container, error)}// attachBackend includes function to implement to provide container attaching functionality.type attachBackend interface {ContainerAttach(name string, c *backend.ContainerAttachConfig) error}// systemBackend includes functions to implement to provide system wide containers functionalitytype systemBackend interface {ContainersPrune(ctx context.Context, pruneFilters filters.Args) (*types.ContainersPruneReport, error)}type commitBackend interface {CreateImageFromContainer(name string, config *backend.CreateImageConfig) (imageID string, err error)}// Backend is all the methods that need to be implemented to provide container specific functionality.type Backend interface {commitBackendexecBackendcopyBackendstateBackendmonitorBackendattachBackendsystemBackend}
Create a Container
Configuration
创建一个容器时,需要根据配置创建一个容器实例,但是,并不运行这个容器实例。创建时需要根据用户配置来设置容器实例,核心配置为 ContainerCreateConfig,主要包含了一般性配置项、主机配置项、网络配置及镜像运行平台配置。
type ContainerCreateConfig struct {Name stringConfig *container.ConfigHostConfig *container.HostConfigNetworkingConfig *network.NetworkingConfigPlatform *specs.PlatformAdjustCPUShares bool}
Config
一般配置中包含了容器配置、运行的基础信息,如:主机名、域名、标准输入输出、环境变量、卷存储信息等等,其定义如下。
type Config struct {Hostname string // HostnameDomainname string // DomainnameUser string // User that will run the command(s) inside the container, also support user:groupAttachStdin bool // Attach the standard input, makes possible user interactionAttachStdout bool // Attach the standard outputAttachStderr bool // Attach the standard errorExposedPorts nat.PortSet `json:",omitempty"` // List of exposed portsTty bool // Attach standard streams to a tty, including stdin if it is not closed.OpenStdin bool // Open stdinStdinOnce bool // If true, close stdin after the 1 attached client disconnects.Env []string // List of environment variable to set in the containerCmd strslice.StrSlice // Command to run when starting the containerHealthcheck *HealthConfig `json:",omitempty"` // Healthcheck describes how to check the container is healthyArgsEscaped bool `json:",omitempty"` // True if command is already escaped (meaning treat as a command line) (Windows specific).Image string // Name of the image as it was passed by the operator (e.g. could be symbolic)Volumes map[string]struct{} // List of volumes (mounts) used for the containerWorkingDir string // Current directory (PWD) in the command will be launchedEntrypoint strslice.StrSlice // Entrypoint to run when starting the containerNetworkDisabled bool `json:",omitempty"` // Is network disabledMacAddress string `json:",omitempty"` // Mac Address of the containerOnBuild []string // ONBUILD metadata that were defined on the image DockerfileLabels map[string]string // List of labels set to this containerStopSignal string `json:",omitempty"` // Signal to stop a containerStopTimeout *int `json:",omitempty"` // Timeout (in seconds) to stop a containerShell strslice.StrSlice `json:",omitempty"` // Shell for shell-form of RUN, CMD, ENTRYPOINT}
Host Config
HostConfig 用于设置容器运行的宿主平台配置选项,这些选项与容器运行的宿主机是强关联的,或者说是容器的非通用设置部分。
type HostConfig struct {// Applicable to all platformsBinds []string // List of volume bindings for this containerContainerIDFile string // File (path) where the containerId is writtenLogConfig LogConfig // Configuration of the logs for this containerNetworkMode NetworkMode // Network mode to use for the containerPortBindings nat.PortMap // Port mapping between the exposed port (container) and the hostRestartPolicy RestartPolicy // Restart policy to be used for the containerAutoRemove bool // Automatically remove container when it exitsVolumeDriver string // Name of the volume driver used to mount volumesVolumesFrom []string // List of volumes to take from other container// Applicable to UNIX platformsCapAdd strslice.StrSlice // List of kernel capabilities to add to the containerCapDrop strslice.StrSlice // List of kernel capabilities to remove from the containerCgroupnsMode CgroupnsMode // Cgroup namespace mode to use for the containerDNS []string `json:"Dns"` // List of DNS server to lookupDNSOptions []string `json:"DnsOptions"` // List of DNSOption to look forDNSSearch []string `json:"DnsSearch"` // List of DNSSearch to look forExtraHosts []string // List of extra hostsGroupAdd []string // List of additional groups that the container process will run asIpcMode IpcMode // IPC namespace to use for the containerCgroup CgroupSpec // Cgroup to use for the containerLinks []string // List of links (in the name:alias form)OomScoreAdj int // Container preference for OOM-killingPidMode PidMode // PID namespace to use for the containerPrivileged bool // Is the container in privileged modePublishAllPorts bool // Should docker publish all exposed port for the containerReadonlyRootfs bool // Is the container root filesystem in read-onlySecurityOpt []string // List of string values to customize labels for MLS systems, such as SELinux.StorageOpt map[string]string `json:",omitempty"` // Storage driver options per container.Tmpfs map[string]string `json:",omitempty"` // List of tmpfs (mounts) used for the containerUTSMode UTSMode // UTS namespace to use for the containerUsernsMode UsernsMode // The user namespace to use for the containerShmSize int64 // Total shm memory usageSysctls map[string]string `json:",omitempty"` // List of Namespaced sysctls used for the containerRuntime string `json:",omitempty"` // Runtime to use with this container// Applicable to WindowsConsoleSize [2]uint // Initial console size (height,width)Isolation Isolation // Isolation technology of the container (e.g. default, hyperv)// Contains container's resources (cgroups, ulimits)Resources// Mounts specs used by the containerMounts []mount.Mount `json:",omitempty"`// MaskedPaths is the list of paths to be masked inside the container (this overrides the default set of paths)MaskedPaths []string// ReadonlyPaths is the list of paths to be set as read-only inside the container (this overrides the default set of paths)ReadonlyPaths []string// Run a custom init inside the container, if null, use the daemon's configured settingsInit *bool `json:",omitempty"`}
Network Config
NetworkingConfig 用于配置容器的 NIC 接口配置,其定义如下所示,EndpointSettings 暂时不在这里展开。
type NetworkingConfig struct {EndpointsConfig map[string]*EndpointSettings // Endpoint configs for each connecting network}
Network Mode
NetworkMode 类型是 string 类型的别名,通过别名附加额外功能可以让代码逻辑变得更加清晰,提升可读性,同时并不影响真实配置项。
// NetworkMode represents the container network stack.type NetworkMode string// IsNone indicates whether container isn't using a network stack.func (n NetworkMode) IsNone() bool {return n == "none"}// IsDefault indicates whether container uses the default network stack.func (n NetworkMode) IsDefault() bool {return n == "default"}// IsPrivate indicates whether container uses its private network stack.func (n NetworkMode) IsPrivate() bool {return !(n.IsHost() || n.IsContainer())}// IsContainer indicates whether container uses a container network stack.func (n NetworkMode) IsContainer() bool {parts := strings.SplitN(string(n), ":", 2)return len(parts) > 1 && parts[0] == "container"}// ConnectedContainer is the id of the container which network this container is connected to.func (n NetworkMode) ConnectedContainer() string {parts := strings.SplitN(string(n), ":", 2)if len(parts) > 1 {return parts[1]}return ""}// UserDefined indicates user-created networkfunc (n NetworkMode) UserDefined() string {if n.IsUserDefined() {return string(n)}return ""}
Container Creation
首先,进行镜像与运行平台检查,如果镜像与运行平台信息不符,提供警告信息。注意触发条件是配置中没有运行平台信息且镜像名称不为空。
if opts.params.Platform == nil && opts.params.Config.Image != "" {if img, _ := daemon.imageService.GetImage(opts.params.Config.Image, opts.params.Platform); img != nil {p := platforms.DefaultSpec()imgPlat := v1.Platform{OS: img.OS,Architecture: img.Architecture,Variant: img.Variant,}if !images.OnlyPlatformWithFallback(p).Match(imgPlat) {warnings = append(warnings, fmt.Sprintf("The requested image's platform (%s) does not match the detected host platform (%s) and no specific platform was requested", platforms.Format(imgPlat), platforms.Format(p)))}}}
接下来,将对网络配置进行检查,检查是否有不正确的 IP 地址配置存在,注意在返回错误信息时,会将报警信息同时返回。
err = verifyNetworkingConfig(opts.params.NetworkingConfig)if err != nil {return containertypes.ContainerCreateCreatedBody{Warnings: warnings}, errdefs.InvalidParameter(err)}
然后,根据宿主机平台进行进一步设置,注意第 4 行的 adaptContainerSettings 是与操作系统相关的方法,需要根据目标操作系统继续跟进。
if opts.params.HostConfig == nil {opts.params.HostConfig = &containertypes.HostConfig{}}err = daemon.adaptContainerSettings(opts.params.HostConfig, opts.params.AdjustCPUShares)
最后,执行容器创建方法。
ctr, err := daemon.create(opts)
Merge Configuration
执行容器创建前,会进一步根据容器用户配置与镜像配置进行配置合并操作,如果用户配置中没有配置诸如 User、ExposedPorts 等等时,会根据镜像默认配置进行补全,细节部分如下。
func merge(userConf, imageConf *containertypes.Config) error {if userConf.User == "" {userConf.User = imageConf.User}if len(userConf.ExposedPorts) == 0 {userConf.ExposedPorts = imageConf.ExposedPorts} else if imageConf.ExposedPorts != nil {for port := range imageConf.ExposedPorts {if _, exists := userConf.ExposedPorts[port]; !exists {userConf.ExposedPorts[port] = struct{}{}}}}if len(userConf.Env) == 0 {userConf.Env = imageConf.Env} else {for _, imageEnv := range imageConf.Env {found := falseimageEnvKey := strings.Split(imageEnv, "=")[0]for _, userEnv := range userConf.Env {userEnvKey := strings.Split(userEnv, "=")[0]if isWindows {// Case insensitive environment variables on WindowsimageEnvKey = strings.ToUpper(imageEnvKey)userEnvKey = strings.ToUpper(userEnvKey)}if imageEnvKey == userEnvKey {found = truebreak}}if !found {userConf.Env = append(userConf.Env, imageEnv)}}}if userConf.Labels == nil {userConf.Labels = map[string]string{}}for l, v := range imageConf.Labels {if _, ok := userConf.Labels[l]; !ok {userConf.Labels[l] = v}}if len(userConf.Entrypoint) == 0 {if len(userConf.Cmd) == 0 {userConf.Cmd = imageConf.Cmd}if userConf.Entrypoint == nil {userConf.Entrypoint = imageConf.Entrypoint}}if imageConf.Healthcheck != nil {if userConf.Healthcheck == nil {userConf.Healthcheck = imageConf.Healthcheck} else {if len(userConf.Healthcheck.Test) == 0 {userConf.Healthcheck.Test = imageConf.Healthcheck.Test}if userConf.Healthcheck.Interval == 0 {userConf.Healthcheck.Interval = imageConf.Healthcheck.Interval}if userConf.Healthcheck.Timeout == 0 {userConf.Healthcheck.Timeout = imageConf.Healthcheck.Timeout}if userConf.Healthcheck.StartPeriod == 0 {userConf.Healthcheck.StartPeriod = imageConf.Healthcheck.StartPeriod}if userConf.Healthcheck.Retries == 0 {userConf.Healthcheck.Retries = imageConf.Healthcheck.Retries}}}if userConf.WorkingDir == "" {userConf.WorkingDir = imageConf.WorkingDir}if len(userConf.Volumes) == 0 {userConf.Volumes = imageConf.Volumes} else {for k, v := range imageConf.Volumes {userConf.Volumes[k] = v}}if userConf.StopSignal == "" {userConf.StopSignal = imageConf.StopSignal}return nil}
ID & Name
创建容器时,会生成一个随机的定长字符串,方法如下所示,注意使用的 rand.Read 方法可以确保这个字符串是安全的随机。
func GenerateRandomID() string {b := make([]byte, 32)for {if _, err := rand.Read(b); err != nil {panic(err) // This shouldn't happen}id := hex.EncodeToString(b)// if we try to parse the truncated for as an int and we don't have// an error then the value is all numeric and causes issues when// used as a hostname. ref #3869if _, err := strconv.ParseInt(TruncateID(id), 10, 64); err == nil {continue}return id}}
如果容器配置中没有设置容器名称,那么会通过如下算法随机生成一个容器名称,第 4 行的过滤有些致敬前辈的意味,这个随机的名称就是我们看到的奇怪的容器名称。
func GetRandomName(retry int) string {begin:name := left[rand.Intn(len(left))] + "_" + right[rand.Intn(len(right))] //nolint:gosec // G404: Use of weak random number generator (math/rand instead of crypto/rand)if name == "boring_wozniak" /* Steve Wozniak is not boring */ {goto begin}if retry > 0 {name += strconv.Itoa(rand.Intn(10)) //nolint:gosec // G404: Use of weak random number generator (math/rand instead of crypto/rand)}return name}
当然,如果还有进一步的兴趣,可以自己去研究一下这些名人信息😊,下面仅摘录一部分。
// Maria Gaetana Agnesi - Italian mathematician, philosopher, theologian and humanitarian. She was the first woman to write a mathematics handbook and the first woman appointed as a Mathematics Professor at a University. https://en.wikipedia.org/wiki/Maria_Gaetana_Agnesi"agnesi",// Muhammad ibn Jābir al-Ḥarrānī al-Battānī was a founding father of astronomy. https://en.wikipedia.org/wiki/Mu%E1%B8%A5ammad_ibn_J%C4%81bir_al-%E1%B8%A4arr%C4%81n%C4%AB_al-Batt%C4%81n%C4%AB"albattani",// Frances E. Allen, became the first female IBM Fellow in 1989. In 2006, she became the first female recipient of the ACM's Turing Award. https://en.wikipedia.org/wiki/Frances_E._Allen"allen",// June Almeida - Scottish virologist who took the first pictures of the rubella virus - https://en.wikipedia.org/wiki/June_Almeida"almeida",// Kathleen Antonelli, American computer programmer and one of the six original programmers of the ENIAC - https://en.wikipedia.org/wiki/Kathleen_Antonelli"antonelli",// Archimedes was a physicist, engineer and mathematician who invented too many things to list them here. https://en.wikipedia.org/wiki/Archimedes"archimedes",
比如理工科都熟悉的阿基米得:
图 2:Archimedes Thoughtful
View DB
在创建容器 ID、Name 时,会使用一个 ViewDB 对这些已经生成的信息进行保存,每次生成时还需要进行检查,确保没有相同的 ID、Name 出现。
err := daemon.containersReplica.ReserveName(name, id);
ViewDB 接口及相关数据结构如下,根据注释信息可以看到 ViewDB 是一个内存数据库,它的实现使用了 github.com/hashicorp/go-memdb。
// ViewDB provides an in-memory transactional (ACID) container Storetype ViewDB interface {Snapshot() ViewSave(*Container) errorDelete(*Container) errorReserveName(name, containerID string) errorReleaseName(name string) error}// View can be used by readers to avoid lockingtype View interface {All() ([]Snapshot, error)Get(id string) (*Snapshot, error)GetID(name string) (string, error)GetAllNames() map[string][]string}type Snapshot struct {types.Container// additional info queries need to filter on// preserve nanosec resolution for queriesCreatedAt time.TimeStartedAt time.TimeName stringPid intExitCode intRunning boolPaused boolManaged boolExposedPorts nat.PortSetPortBindings nat.PortSetHealth stringHostConfig struct {Isolation string}}
Container
接下来就是容器实例的创建及配置工作了,首先是容器根目录,简单通过 daemon.repository 和容器 ID 拼接而成。
func (daemon *Daemon) containerRoot(id string) string {return filepath.Join(daemon.repository, id)}
然后通过 NewBaseContainer 创建 Container 实例
func NewBaseContainer(id, root string) *Container {return &Container{ID: id,State: NewState(),ExecCommands: exec.NewStore(),Root: root,MountPoints: make(map[string]*volumemounts.MountPoint),StreamConfig: stream.NewConfig(),attachContext: &attachContext{},}}
最后,配置 Container 实例其他域成员,下面代码中的 base 就是创建的 Container 实例。
base := daemon.newBaseContainer(id)base.Created = time.Now().UTC()base.Managed = managedbase.Path = entrypointbase.Args = args // FIXME: de-duplicate from configbase.Config = configbase.HostConfig = &containertypes.HostConfig{}base.ImageID = imgIDbase.NetworkSettings = &network.Settings{IsAnonymousEndpoint: noExplicitName}base.Name = namebase.Driver = daemon.imageService.GraphDriverName()base.OS = operatingSystem
RW Layer
容器实例创建完毕后,需要准备与该容器实例运行时需要的可写层,可写层通过 Daemon 常驻进程实例中的 ImageService 实现。
rwLayer, err := daemon.imageService.CreateLayer(ctr, setupInitLayer(daemon.idMapping))
创建可写层主要依赖 layer.Store 接口的实现,通过镜像获取到的 ChainID 做为 CreateRWLayer 的 parent 参数,如图 3 所示。
图 3:CreateRWLayer 示意图
layer.ChainID 类型是 digest.Digest 的别名,也就是最终仍然是一个字符串,可以用于唯一标识一个镜像。
// ChainID is the content-addressable ID of a layer.type ChainID digest.Digest
Layer Store
CreateLayer 方法是创建可写层的唯一方法,第 19 的 container.ID 目前是一个 SHA256 摘要信息,也是容器唯一标识信息,这样可以保证可写层与容器的唯一对应关系。
func (i *ImageService) CreateLayer(container *container.Container, initFunc layer.MountInit) (layer.RWLayer, error) {var layerID layer.ChainIDif container.ImageID != "" {img, err := i.imageStore.Get(container.ImageID)if err != nil {return nil, err}layerID = img.RootFS.ChainID()}rwLayerOpts := &layer.CreateRWLayerOpts{MountLabel: container.MountLabel,InitFunc: initFunc,StorageOpt: container.HostConfig.StorageOpt,}// Indexing by OS is safe here as validation of OS has already been performed in create() (the only// caller), and guaranteed non-nilreturn i.layerStore.CreateRWLayer(container.ID, layerID, rwLayerOpts)}
在创建 layer.Store 实例时,newStoreFromGraphDriver 是核心方法,这个方法会创建存在于宿主机文件系统中的 Metadata Store 实例,并与 Graph Driver 关联。创建过程中,会将已经存在的层进行加载,见代码 26 ~ 41 行。
func newStoreFromGraphDriver(root string, driver graphdriver.Driver) (Store, error) {caps := graphdriver.Capabilities{}if capDriver, ok := driver.(graphdriver.CapabilityDriver); ok {caps = capDriver.Capabilities()}ms, err := newFSMetadataStore(root)if err != nil {return nil, err}ls := &layerStore{store: ms,driver: driver,layerMap: map[ChainID]*roLayer{},mounts: map[string]*mountedLayer{},locker: locker.New(),useTarSplit: !caps.ReproducesExactDiffs,}ids, mounts, err := ms.List()if err != nil {return nil, err}for _, id := range ids {l, err := ls.loadLayer(id)if err != nil {logrus.Debugf("Failed to load layer %s: %s", id, err)continue}if l.parent != nil {l.parent.referenceCount++}}for _, mount := range mounts {if err := ls.loadMount(mount); err != nil {logrus.Debugf("Failed to load mount %s: %s", mount, err)}}return ls, nil}
创建过程关键部分如图 4 所示。
图 4:layer.Store 创建示意图
Setup Initial Layer
执行初始化操作的 setupInitialLayer 是与宿主机操作系统相关的方法,以 Unix 版本实现为例,这个方法返回一个闭包方法来执行最终的初始化操作。
func setupInitLayer(idMapping *idtools.IdentityMapping) func(containerfs.ContainerFS) error {return func(initPath containerfs.ContainerFS) error {return initlayer.Setup(initPath, idMapping.RootPair())}}
Unix 版本的 Setup 操作主要是对文件、目录、符号链接等进行创建并设置合理的权限,详细步骤如下所示。
func Setup(initLayerFs containerfs.ContainerFS, rootIdentity idtools.Identity) error {// Since all paths are local to the container, we can just extract initLayerFs.Path()initLayer := initLayerFs.Path()for pth, typ := range map[string]string{"/dev/pts": "dir","/dev/shm": "dir","/proc": "dir","/sys": "dir","/.dockerenv": "file","/etc/resolv.conf": "file","/etc/hosts": "file","/etc/hostname": "file","/dev/console": "file","/etc/mtab": "/proc/mounts",} {parts := strings.Split(pth, "/")prev := "/"for _, p := range parts[1:] {prev = filepath.Join(prev, p)unix.Unlink(filepath.Join(initLayer, prev))}if _, err := os.Stat(filepath.Join(initLayer, pth)); err != nil {if os.IsNotExist(err) {if err := idtools.MkdirAllAndChownNew(filepath.Join(initLayer, filepath.Dir(pth)), 0755, rootIdentity); err != nil {return err}switch typ {case "dir":if err := idtools.MkdirAllAndChownNew(filepath.Join(initLayer, pth), 0755, rootIdentity); err != nil {return err}case "file":f, err := os.OpenFile(filepath.Join(initLayer, pth), os.O_CREATE, 0755)if err != nil {return err}f.Chown(rootIdentity.UID, rootIdentity.GID)f.Close()default:if err := os.Symlink(typ, filepath.Join(initLayer, pth)); err != nil {return err}}} else {return err}}}// Layer is ready to use, if it wasn't before.return nil}
Start a Container
Containerd
启动容器的过程中,会使用到 containerd,图 5 以 Unix 版本实现中第一步创建容器(确实是在运行容器过程中创建)为例展示 Daemon 实例与 containerd 的交互过程。
图 5:containerd 交互示意图
创建 containerd 的代码在 initLibcontainerd 中,下面代码为 Unix 版本实现。
func (daemon *Daemon) initLibcontainerd(ctx context.Context) error {var err errordaemon.containerd, err = remote.NewClient(ctx,daemon.containerdCli,filepath.Join(daemon.configStore.ExecRoot, "containerd"),daemon.configStore.ContainerdNamespace,daemon,)return err}
在 containerd 中的接口类型 containers.Store 用于与容器底层数据库进行交互。
type Store interface {// Get a container using the id.//// Container object is returned on success. If the id is not known to the// store, an error will be returned.Get(ctx context.Context, id string) (Container, error)// List returns containers that match one or more of the provided filters.List(ctx context.Context, filters ...string) ([]Container, error)// Create a container in the store from the provided container.Create(ctx context.Context, container Container) (Container, error)// Update the container with the provided container object. ID must be set.//// If one or more fieldpaths are provided, only the field corresponding to// the fieldpaths will be mutated.Update(ctx context.Context, container Container, fieldpaths ...string) (Container, error)// Delete a container using the id.//// nil will be returned on success. If the container is not known to the// store, ErrNotFound will be returned.Delete(ctx context.Context, id string) error}
最终的 Create 方法通过 GRPC 调用执行。
func (c *containersClient) Create(ctx context.Context, in *CreateContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error) {out := new(CreateContainerResponse)err := c.cc.Invoke(ctx, "/containerd.services.containers.v1.Containers/Create", in, out, opts...)if err != nil {return nil, err}return out, nil}
