2018-02-05 21:05:59 +00:00
|
|
|
package containerd // import "github.com/docker/docker/plugin/executor/containerd"
|
2017-07-14 20:45:32 +00:00
|
|
|
|
|
|
|
import (
|
2017-09-22 13:52:41 +00:00
|
|
|
"context"
|
2017-07-14 20:45:32 +00:00
|
|
|
"io"
|
2017-09-22 13:52:41 +00:00
|
|
|
"sync"
|
2017-07-14 20:45:32 +00:00
|
|
|
|
2018-05-23 19:15:21 +00:00
|
|
|
"github.com/containerd/containerd"
|
2017-11-30 00:15:20 +00:00
|
|
|
"github.com/containerd/containerd/cio"
|
2020-07-07 20:33:46 +00:00
|
|
|
"github.com/docker/docker/api/types"
|
2018-01-11 19:53:06 +00:00
|
|
|
"github.com/docker/docker/errdefs"
|
2017-07-14 20:45:32 +00:00
|
|
|
"github.com/docker/docker/libcontainerd"
|
Windows: Experimental: Allow containerd for runtime
Signed-off-by: John Howard <jhoward@microsoft.com>
This is the first step in refactoring moby (dockerd) to use containerd on Windows.
Similar to the current model in Linux, this adds the option to enable it for runtime.
It does not switch the graphdriver to containerd snapshotters.
- Refactors libcontainerd to a series of subpackages so that either a
"local" containerd (1) or a "remote" (2) containerd can be loaded as opposed
to conditional compile as "local" for Windows and "remote" for Linux.
- Updates libcontainerd such that Windows has an option to allow the use of a
"remote" containerd. Here, it communicates over a named pipe using GRPC.
This is currently guarded behind the experimental flag, an environment variable,
and the providing of a pipename to connect to containerd.
- Infrastructure pieces such as under pkg/system to have helper functions for
determining whether containerd is being used.
(1) "local" containerd is what the daemon on Windows has used since inception.
It's not really containerd at all - it's simply local invocation of HCS APIs
directly in-process from the daemon through the Microsoft/hcsshim library.
(2) "remote" containerd is what docker on Linux uses for it's runtime. It means
that there is a separate containerd service running, and docker communicates over
GRPC to it.
To try this out, you will need to start with something like the following:
Window 1:
containerd --log-level debug
Window 2:
$env:DOCKER_WINDOWS_CONTAINERD=1
dockerd --experimental -D --containerd \\.\pipe\containerd-containerd
You will need the following binary from github.com/containerd/containerd in your path:
- containerd.exe
You will need the following binaries from github.com/Microsoft/hcsshim in your path:
- runhcs.exe
- containerd-shim-runhcs-v1.exe
For LCOW, it will require and initrd.img and kernel in `C:\Program Files\Linux Containers`.
This is no different to the current requirements. However, you may need updated binaries,
particularly initrd.img built from Microsoft/opengcs as (at the time of writing), Linuxkit
binaries are somewhat out of date.
Note that containerd and hcsshim for HCS v2 APIs do not yet support all the required
functionality needed for docker. This will come in time - this is a baby (although large)
step to migrating Docker on Windows to containerd.
Note that the HCS v2 APIs are only called on RS5+ builds. RS1..RS4 will still use
HCS v1 APIs as the v2 APIs were not fully developed enough on these builds to be usable.
This abstraction is done in HCSShim. (Referring specifically to runtime)
Note the LCOW graphdriver still uses HCS v1 APIs regardless.
Note also that this does not migrate docker to use containerd snapshotters
rather than graphdrivers. This needs to be done in conjunction with Linux also
doing the same switch.
2019-01-08 22:30:52 +00:00
|
|
|
libcontainerdtypes "github.com/docker/docker/libcontainerd/types"
|
2019-08-05 14:37:47 +00:00
|
|
|
specs "github.com/opencontainers/runtime-spec/specs-go"
|
2017-07-14 20:45:32 +00:00
|
|
|
"github.com/pkg/errors"
|
2017-09-22 13:52:41 +00:00
|
|
|
"github.com/sirupsen/logrus"
|
2017-07-14 20:45:32 +00:00
|
|
|
)
|
|
|
|
|
2018-05-23 19:15:21 +00:00
|
|
|
// PluginNamespace is the name used for the plugins namespace
|
|
|
|
const PluginNamespace = "plugins.moby"
|
2017-09-22 13:52:41 +00:00
|
|
|
|
2017-07-14 20:45:32 +00:00
|
|
|
// ExitHandler represents an object that is called when the exit event is received from containerd
|
|
|
|
type ExitHandler interface {
|
|
|
|
HandleExitEvent(id string) error
|
|
|
|
}
|
|
|
|
|
|
|
|
// New creates a new containerd plugin executor
|
2020-07-07 20:33:46 +00:00
|
|
|
func New(ctx context.Context, rootDir string, cli *containerd.Client, ns string, exitHandler ExitHandler, runtime types.Runtime) (*Executor, error) {
|
2017-09-22 13:52:41 +00:00
|
|
|
e := &Executor{
|
|
|
|
rootDir: rootDir,
|
|
|
|
exitHandler: exitHandler,
|
2020-07-07 20:33:46 +00:00
|
|
|
runtime: runtime,
|
2017-09-22 13:52:41 +00:00
|
|
|
}
|
2018-05-23 19:15:21 +00:00
|
|
|
|
2020-07-07 20:33:46 +00:00
|
|
|
client, err := libcontainerd.NewClient(ctx, cli, rootDir, ns, e)
|
2017-07-14 20:45:32 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "error creating containerd exec client")
|
|
|
|
}
|
|
|
|
e.client = client
|
|
|
|
return e, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Executor is the containerd client implementation of a plugin executor
|
|
|
|
type Executor struct {
|
2017-09-22 13:52:41 +00:00
|
|
|
rootDir string
|
2019-03-25 20:17:17 +00:00
|
|
|
client libcontainerdtypes.Client
|
2017-07-14 20:45:32 +00:00
|
|
|
exitHandler ExitHandler
|
2020-07-07 20:33:46 +00:00
|
|
|
runtime types.Runtime
|
2017-07-14 20:45:32 +00:00
|
|
|
}
|
|
|
|
|
2018-04-20 14:48:54 +00:00
|
|
|
// deleteTaskAndContainer deletes plugin task and then plugin container from containerd
|
2019-03-25 20:17:17 +00:00
|
|
|
func deleteTaskAndContainer(ctx context.Context, cli libcontainerdtypes.Client, id string, p libcontainerdtypes.Process) {
|
|
|
|
if p != nil {
|
|
|
|
if _, _, err := p.Delete(ctx); err != nil && !errdefs.IsNotFound(err) {
|
|
|
|
logrus.WithError(err).WithField("id", id).Error("failed to delete plugin task from containerd")
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if _, _, err := cli.DeleteTask(ctx, id); err != nil && !errdefs.IsNotFound(err) {
|
|
|
|
logrus.WithError(err).WithField("id", id).Error("failed to delete plugin task from containerd")
|
|
|
|
}
|
2018-04-20 14:48:54 +00:00
|
|
|
}
|
|
|
|
|
2019-03-25 20:17:17 +00:00
|
|
|
if err := cli.Delete(ctx, id); err != nil && !errdefs.IsNotFound(err) {
|
2018-04-20 14:48:54 +00:00
|
|
|
logrus.WithError(err).WithField("id", id).Error("failed to delete plugin container from containerd")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-14 20:45:32 +00:00
|
|
|
// Create creates a new container
|
|
|
|
func (e *Executor) Create(id string, spec specs.Spec, stdout, stderr io.WriteCloser) error {
|
2017-09-22 13:52:41 +00:00
|
|
|
ctx := context.Background()
|
2020-07-07 20:33:46 +00:00
|
|
|
err := e.client.Create(ctx, id, &spec, e.runtime.Shim.Binary, e.runtime.Shim.Opts)
|
2017-09-22 13:52:41 +00:00
|
|
|
if err != nil {
|
2018-03-27 14:03:53 +00:00
|
|
|
status, err2 := e.client.Status(ctx, id)
|
|
|
|
if err2 != nil {
|
|
|
|
if !errdefs.IsNotFound(err2) {
|
|
|
|
logrus.WithError(err2).WithField("id", id).Warn("Received an error while attempting to read plugin status")
|
|
|
|
}
|
|
|
|
} else {
|
2019-04-04 18:46:02 +00:00
|
|
|
if status != containerd.Running && status != containerd.Unknown {
|
2018-03-27 14:03:53 +00:00
|
|
|
if err2 := e.client.Delete(ctx, id); err2 != nil && !errdefs.IsNotFound(err2) {
|
|
|
|
logrus.WithError(err2).WithField("plugin", id).Error("Error cleaning up containerd container")
|
|
|
|
}
|
2020-07-07 20:33:46 +00:00
|
|
|
err = e.client.Create(ctx, id, &spec, e.runtime.Shim.Binary, e.runtime.Shim.Opts)
|
2018-03-27 14:03:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "error creating containerd container")
|
|
|
|
}
|
2017-09-22 13:52:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_, err = e.client.Start(ctx, id, "", false, attachStreamsFunc(stdout, stderr))
|
2018-03-27 14:03:53 +00:00
|
|
|
if err != nil {
|
2019-03-25 20:17:17 +00:00
|
|
|
deleteTaskAndContainer(ctx, e.client, id, nil)
|
2018-03-27 14:03:53 +00:00
|
|
|
}
|
2017-09-22 13:52:41 +00:00
|
|
|
return err
|
2017-07-14 20:45:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Restore restores a container
|
2018-04-20 14:48:54 +00:00
|
|
|
func (e *Executor) Restore(id string, stdout, stderr io.WriteCloser) (bool, error) {
|
2019-03-25 20:17:17 +00:00
|
|
|
alive, _, p, err := e.client.Restore(context.Background(), id, attachStreamsFunc(stdout, stderr))
|
2017-09-22 13:52:41 +00:00
|
|
|
if err != nil && !errdefs.IsNotFound(err) {
|
2018-04-20 14:48:54 +00:00
|
|
|
return false, err
|
2017-09-22 13:52:41 +00:00
|
|
|
}
|
|
|
|
if !alive {
|
2019-03-25 20:17:17 +00:00
|
|
|
deleteTaskAndContainer(context.Background(), e.client, id, p)
|
2017-09-22 13:52:41 +00:00
|
|
|
}
|
2018-04-20 14:48:54 +00:00
|
|
|
return alive, nil
|
2017-07-14 20:45:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// IsRunning returns if the container with the given id is running
|
|
|
|
func (e *Executor) IsRunning(id string) (bool, error) {
|
2017-09-22 13:52:41 +00:00
|
|
|
status, err := e.client.Status(context.Background(), id)
|
2019-04-04 18:46:02 +00:00
|
|
|
return status == containerd.Running, err
|
2017-07-14 20:45:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Signal sends the specified signal to the container
|
|
|
|
func (e *Executor) Signal(id string, signal int) error {
|
Windows: Experimental: Allow containerd for runtime
Signed-off-by: John Howard <jhoward@microsoft.com>
This is the first step in refactoring moby (dockerd) to use containerd on Windows.
Similar to the current model in Linux, this adds the option to enable it for runtime.
It does not switch the graphdriver to containerd snapshotters.
- Refactors libcontainerd to a series of subpackages so that either a
"local" containerd (1) or a "remote" (2) containerd can be loaded as opposed
to conditional compile as "local" for Windows and "remote" for Linux.
- Updates libcontainerd such that Windows has an option to allow the use of a
"remote" containerd. Here, it communicates over a named pipe using GRPC.
This is currently guarded behind the experimental flag, an environment variable,
and the providing of a pipename to connect to containerd.
- Infrastructure pieces such as under pkg/system to have helper functions for
determining whether containerd is being used.
(1) "local" containerd is what the daemon on Windows has used since inception.
It's not really containerd at all - it's simply local invocation of HCS APIs
directly in-process from the daemon through the Microsoft/hcsshim library.
(2) "remote" containerd is what docker on Linux uses for it's runtime. It means
that there is a separate containerd service running, and docker communicates over
GRPC to it.
To try this out, you will need to start with something like the following:
Window 1:
containerd --log-level debug
Window 2:
$env:DOCKER_WINDOWS_CONTAINERD=1
dockerd --experimental -D --containerd \\.\pipe\containerd-containerd
You will need the following binary from github.com/containerd/containerd in your path:
- containerd.exe
You will need the following binaries from github.com/Microsoft/hcsshim in your path:
- runhcs.exe
- containerd-shim-runhcs-v1.exe
For LCOW, it will require and initrd.img and kernel in `C:\Program Files\Linux Containers`.
This is no different to the current requirements. However, you may need updated binaries,
particularly initrd.img built from Microsoft/opengcs as (at the time of writing), Linuxkit
binaries are somewhat out of date.
Note that containerd and hcsshim for HCS v2 APIs do not yet support all the required
functionality needed for docker. This will come in time - this is a baby (although large)
step to migrating Docker on Windows to containerd.
Note that the HCS v2 APIs are only called on RS5+ builds. RS1..RS4 will still use
HCS v1 APIs as the v2 APIs were not fully developed enough on these builds to be usable.
This abstraction is done in HCSShim. (Referring specifically to runtime)
Note the LCOW graphdriver still uses HCS v1 APIs regardless.
Note also that this does not migrate docker to use containerd snapshotters
rather than graphdrivers. This needs to be done in conjunction with Linux also
doing the same switch.
2019-01-08 22:30:52 +00:00
|
|
|
return e.client.SignalProcess(context.Background(), id, libcontainerdtypes.InitProcessName, signal)
|
2017-07-14 20:45:32 +00:00
|
|
|
}
|
|
|
|
|
2017-09-22 13:52:41 +00:00
|
|
|
// ProcessEvent handles events from containerd
|
2017-07-14 20:45:32 +00:00
|
|
|
// All events are ignored except the exit event, which is sent of to the stored handler
|
Windows: Experimental: Allow containerd for runtime
Signed-off-by: John Howard <jhoward@microsoft.com>
This is the first step in refactoring moby (dockerd) to use containerd on Windows.
Similar to the current model in Linux, this adds the option to enable it for runtime.
It does not switch the graphdriver to containerd snapshotters.
- Refactors libcontainerd to a series of subpackages so that either a
"local" containerd (1) or a "remote" (2) containerd can be loaded as opposed
to conditional compile as "local" for Windows and "remote" for Linux.
- Updates libcontainerd such that Windows has an option to allow the use of a
"remote" containerd. Here, it communicates over a named pipe using GRPC.
This is currently guarded behind the experimental flag, an environment variable,
and the providing of a pipename to connect to containerd.
- Infrastructure pieces such as under pkg/system to have helper functions for
determining whether containerd is being used.
(1) "local" containerd is what the daemon on Windows has used since inception.
It's not really containerd at all - it's simply local invocation of HCS APIs
directly in-process from the daemon through the Microsoft/hcsshim library.
(2) "remote" containerd is what docker on Linux uses for it's runtime. It means
that there is a separate containerd service running, and docker communicates over
GRPC to it.
To try this out, you will need to start with something like the following:
Window 1:
containerd --log-level debug
Window 2:
$env:DOCKER_WINDOWS_CONTAINERD=1
dockerd --experimental -D --containerd \\.\pipe\containerd-containerd
You will need the following binary from github.com/containerd/containerd in your path:
- containerd.exe
You will need the following binaries from github.com/Microsoft/hcsshim in your path:
- runhcs.exe
- containerd-shim-runhcs-v1.exe
For LCOW, it will require and initrd.img and kernel in `C:\Program Files\Linux Containers`.
This is no different to the current requirements. However, you may need updated binaries,
particularly initrd.img built from Microsoft/opengcs as (at the time of writing), Linuxkit
binaries are somewhat out of date.
Note that containerd and hcsshim for HCS v2 APIs do not yet support all the required
functionality needed for docker. This will come in time - this is a baby (although large)
step to migrating Docker on Windows to containerd.
Note that the HCS v2 APIs are only called on RS5+ builds. RS1..RS4 will still use
HCS v1 APIs as the v2 APIs were not fully developed enough on these builds to be usable.
This abstraction is done in HCSShim. (Referring specifically to runtime)
Note the LCOW graphdriver still uses HCS v1 APIs regardless.
Note also that this does not migrate docker to use containerd snapshotters
rather than graphdrivers. This needs to be done in conjunction with Linux also
doing the same switch.
2019-01-08 22:30:52 +00:00
|
|
|
func (e *Executor) ProcessEvent(id string, et libcontainerdtypes.EventType, ei libcontainerdtypes.EventInfo) error {
|
2017-09-22 13:52:41 +00:00
|
|
|
switch et {
|
Windows: Experimental: Allow containerd for runtime
Signed-off-by: John Howard <jhoward@microsoft.com>
This is the first step in refactoring moby (dockerd) to use containerd on Windows.
Similar to the current model in Linux, this adds the option to enable it for runtime.
It does not switch the graphdriver to containerd snapshotters.
- Refactors libcontainerd to a series of subpackages so that either a
"local" containerd (1) or a "remote" (2) containerd can be loaded as opposed
to conditional compile as "local" for Windows and "remote" for Linux.
- Updates libcontainerd such that Windows has an option to allow the use of a
"remote" containerd. Here, it communicates over a named pipe using GRPC.
This is currently guarded behind the experimental flag, an environment variable,
and the providing of a pipename to connect to containerd.
- Infrastructure pieces such as under pkg/system to have helper functions for
determining whether containerd is being used.
(1) "local" containerd is what the daemon on Windows has used since inception.
It's not really containerd at all - it's simply local invocation of HCS APIs
directly in-process from the daemon through the Microsoft/hcsshim library.
(2) "remote" containerd is what docker on Linux uses for it's runtime. It means
that there is a separate containerd service running, and docker communicates over
GRPC to it.
To try this out, you will need to start with something like the following:
Window 1:
containerd --log-level debug
Window 2:
$env:DOCKER_WINDOWS_CONTAINERD=1
dockerd --experimental -D --containerd \\.\pipe\containerd-containerd
You will need the following binary from github.com/containerd/containerd in your path:
- containerd.exe
You will need the following binaries from github.com/Microsoft/hcsshim in your path:
- runhcs.exe
- containerd-shim-runhcs-v1.exe
For LCOW, it will require and initrd.img and kernel in `C:\Program Files\Linux Containers`.
This is no different to the current requirements. However, you may need updated binaries,
particularly initrd.img built from Microsoft/opengcs as (at the time of writing), Linuxkit
binaries are somewhat out of date.
Note that containerd and hcsshim for HCS v2 APIs do not yet support all the required
functionality needed for docker. This will come in time - this is a baby (although large)
step to migrating Docker on Windows to containerd.
Note that the HCS v2 APIs are only called on RS5+ builds. RS1..RS4 will still use
HCS v1 APIs as the v2 APIs were not fully developed enough on these builds to be usable.
This abstraction is done in HCSShim. (Referring specifically to runtime)
Note the LCOW graphdriver still uses HCS v1 APIs regardless.
Note also that this does not migrate docker to use containerd snapshotters
rather than graphdrivers. This needs to be done in conjunction with Linux also
doing the same switch.
2019-01-08 22:30:52 +00:00
|
|
|
case libcontainerdtypes.EventExit:
|
2019-03-25 20:17:17 +00:00
|
|
|
deleteTaskAndContainer(context.Background(), e.client, id, nil)
|
2017-09-22 13:52:41 +00:00
|
|
|
return e.exitHandler.HandleExitEvent(ei.ContainerID)
|
2017-07-14 20:45:32 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-11-30 00:15:20 +00:00
|
|
|
type rio struct {
|
|
|
|
cio.IO
|
2017-09-22 13:52:41 +00:00
|
|
|
|
|
|
|
wg sync.WaitGroup
|
|
|
|
}
|
|
|
|
|
2017-11-30 00:15:20 +00:00
|
|
|
func (c *rio) Wait() {
|
2017-09-22 13:52:41 +00:00
|
|
|
c.wg.Wait()
|
|
|
|
c.IO.Wait()
|
|
|
|
}
|
|
|
|
|
Windows: Experimental: Allow containerd for runtime
Signed-off-by: John Howard <jhoward@microsoft.com>
This is the first step in refactoring moby (dockerd) to use containerd on Windows.
Similar to the current model in Linux, this adds the option to enable it for runtime.
It does not switch the graphdriver to containerd snapshotters.
- Refactors libcontainerd to a series of subpackages so that either a
"local" containerd (1) or a "remote" (2) containerd can be loaded as opposed
to conditional compile as "local" for Windows and "remote" for Linux.
- Updates libcontainerd such that Windows has an option to allow the use of a
"remote" containerd. Here, it communicates over a named pipe using GRPC.
This is currently guarded behind the experimental flag, an environment variable,
and the providing of a pipename to connect to containerd.
- Infrastructure pieces such as under pkg/system to have helper functions for
determining whether containerd is being used.
(1) "local" containerd is what the daemon on Windows has used since inception.
It's not really containerd at all - it's simply local invocation of HCS APIs
directly in-process from the daemon through the Microsoft/hcsshim library.
(2) "remote" containerd is what docker on Linux uses for it's runtime. It means
that there is a separate containerd service running, and docker communicates over
GRPC to it.
To try this out, you will need to start with something like the following:
Window 1:
containerd --log-level debug
Window 2:
$env:DOCKER_WINDOWS_CONTAINERD=1
dockerd --experimental -D --containerd \\.\pipe\containerd-containerd
You will need the following binary from github.com/containerd/containerd in your path:
- containerd.exe
You will need the following binaries from github.com/Microsoft/hcsshim in your path:
- runhcs.exe
- containerd-shim-runhcs-v1.exe
For LCOW, it will require and initrd.img and kernel in `C:\Program Files\Linux Containers`.
This is no different to the current requirements. However, you may need updated binaries,
particularly initrd.img built from Microsoft/opengcs as (at the time of writing), Linuxkit
binaries are somewhat out of date.
Note that containerd and hcsshim for HCS v2 APIs do not yet support all the required
functionality needed for docker. This will come in time - this is a baby (although large)
step to migrating Docker on Windows to containerd.
Note that the HCS v2 APIs are only called on RS5+ builds. RS1..RS4 will still use
HCS v1 APIs as the v2 APIs were not fully developed enough on these builds to be usable.
This abstraction is done in HCSShim. (Referring specifically to runtime)
Note the LCOW graphdriver still uses HCS v1 APIs regardless.
Note also that this does not migrate docker to use containerd snapshotters
rather than graphdrivers. This needs to be done in conjunction with Linux also
doing the same switch.
2019-01-08 22:30:52 +00:00
|
|
|
func attachStreamsFunc(stdout, stderr io.WriteCloser) libcontainerdtypes.StdioCallback {
|
2017-12-07 19:26:27 +00:00
|
|
|
return func(iop *cio.DirectIO) (cio.IO, error) {
|
2017-09-22 13:52:41 +00:00
|
|
|
if iop.Stdin != nil {
|
|
|
|
iop.Stdin.Close()
|
|
|
|
// closing stdin shouldn't be needed here, it should never be open
|
|
|
|
panic("plugin stdin shouldn't have been created!")
|
|
|
|
}
|
|
|
|
|
2017-11-30 00:15:20 +00:00
|
|
|
rio := &rio{IO: iop}
|
|
|
|
rio.wg.Add(2)
|
2017-07-14 20:45:32 +00:00
|
|
|
go func() {
|
|
|
|
io.Copy(stdout, iop.Stdout)
|
|
|
|
stdout.Close()
|
2017-11-30 00:15:20 +00:00
|
|
|
rio.wg.Done()
|
2017-07-14 20:45:32 +00:00
|
|
|
}()
|
|
|
|
go func() {
|
|
|
|
io.Copy(stderr, iop.Stderr)
|
|
|
|
stderr.Close()
|
2017-11-30 00:15:20 +00:00
|
|
|
rio.wg.Done()
|
2017-07-14 20:45:32 +00:00
|
|
|
}()
|
2017-11-30 00:15:20 +00:00
|
|
|
return rio, nil
|
2017-07-14 20:45:32 +00:00
|
|
|
}
|
|
|
|
}
|