Merge component 'engine' from git@github.com:moby/moby master

This commit is contained in:
GordonTheTurtle
2018-05-15 17:07:29 +00:00
34 changed files with 663 additions and 653 deletions

View File

@ -22,7 +22,7 @@ func (v *volumeRouter) getVolumesList(ctx context.Context, w http.ResponseWriter
if err != nil {
return err
}
return httputils.WriteJSON(w, http.StatusOK, &volumetypes.VolumesListOKBody{Volumes: volumes, Warnings: warnings})
return httputils.WriteJSON(w, http.StatusOK, &volumetypes.VolumeListOKBody{Volumes: volumes, Warnings: warnings})
}
func (v *volumeRouter) getVolumeByName(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
@ -46,7 +46,7 @@ func (v *volumeRouter) postVolumesCreate(ctx context.Context, w http.ResponseWri
return err
}
var req volumetypes.VolumesCreateBody
var req volumetypes.VolumeCreateBody
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
if err == io.EOF {
return errdefs.InvalidParameter(errors.New("got EOF while reading request body"))

View File

@ -1,4 +1,4 @@
package volume // import "github.com/docker/docker/api/types/volume"
package volume
// ----------------------------------------------------------------------------
// DO NOT EDIT THIS FILE
@ -7,9 +7,9 @@ package volume // import "github.com/docker/docker/api/types/volume"
// See hack/generate-swagger-api.sh
// ----------------------------------------------------------------------------
// VolumesCreateBody volumes create body
// swagger:model VolumesCreateBody
type VolumesCreateBody struct {
// VolumeCreateBody
// swagger:model VolumeCreateBody
type VolumeCreateBody struct {
// Name of the volume driver to use.
// Required: true

View File

@ -1,4 +1,4 @@
package volume // import "github.com/docker/docker/api/types/volume"
package volume
// ----------------------------------------------------------------------------
// DO NOT EDIT THIS FILE
@ -9,9 +9,9 @@ package volume // import "github.com/docker/docker/api/types/volume"
import "github.com/docker/docker/api/types"
// VolumesListOKBody volumes list o k body
// swagger:model VolumesListOKBody
type VolumesListOKBody struct {
// VolumeListOKBody
// swagger:model VolumeListOKBody
type VolumeListOKBody struct {
// List of volumes
// Required: true

View File

@ -168,10 +168,10 @@ type SystemAPIClient interface {
// VolumeAPIClient defines API client methods for the volumes
type VolumeAPIClient interface {
VolumeCreate(ctx context.Context, options volumetypes.VolumesCreateBody) (types.Volume, error)
VolumeCreate(ctx context.Context, options volumetypes.VolumeCreateBody) (types.Volume, error)
VolumeInspect(ctx context.Context, volumeID string) (types.Volume, error)
VolumeInspectWithRaw(ctx context.Context, volumeID string) (types.Volume, []byte, error)
VolumeList(ctx context.Context, filter filters.Args) (volumetypes.VolumesListOKBody, error)
VolumeList(ctx context.Context, filter filters.Args) (volumetypes.VolumeListOKBody, error)
VolumeRemove(ctx context.Context, volumeID string, force bool) error
VolumesPrune(ctx context.Context, pruneFilter filters.Args) (types.VolumesPruneReport, error)
}

View File

@ -9,7 +9,7 @@ import (
)
// VolumeCreate creates a volume in the docker host.
func (cli *Client) VolumeCreate(ctx context.Context, options volumetypes.VolumesCreateBody) (types.Volume, error) {
func (cli *Client) VolumeCreate(ctx context.Context, options volumetypes.VolumeCreateBody) (types.Volume, error) {
var volume types.Volume
resp, err := cli.post(ctx, "/volumes/create", nil, options, nil)
if err != nil {

View File

@ -19,7 +19,7 @@ func TestVolumeCreateError(t *testing.T) {
client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
}
_, err := client.VolumeCreate(context.Background(), volumetypes.VolumesCreateBody{})
_, err := client.VolumeCreate(context.Background(), volumetypes.VolumeCreateBody{})
if err == nil || err.Error() != "Error response from daemon: Server error" {
t.Fatalf("expected a Server Error, got %v", err)
}
@ -53,7 +53,7 @@ func TestVolumeCreate(t *testing.T) {
}),
}
volume, err := client.VolumeCreate(context.Background(), volumetypes.VolumesCreateBody{
volume, err := client.VolumeCreate(context.Background(), volumetypes.VolumeCreateBody{
Name: "myvolume",
Driver: "mydriver",
DriverOpts: map[string]string{

View File

@ -10,8 +10,8 @@ import (
)
// VolumeList returns the volumes configured in the docker host.
func (cli *Client) VolumeList(ctx context.Context, filter filters.Args) (volumetypes.VolumesListOKBody, error) {
var volumes volumetypes.VolumesListOKBody
func (cli *Client) VolumeList(ctx context.Context, filter filters.Args) (volumetypes.VolumeListOKBody, error) {
var volumes volumetypes.VolumeListOKBody
query := url.Values{}
if filter.Len() > 0 {

View File

@ -69,7 +69,7 @@ func TestVolumeList(t *testing.T) {
if actualFilters != listCase.expectedFilters {
return nil, fmt.Errorf("filters not set in URL query properly. Expected '%s', got %s", listCase.expectedFilters, actualFilters)
}
content, err := json.Marshal(volumetypes.VolumesListOKBody{
content, err := json.Marshal(volumetypes.VolumeListOKBody{
Volumes: []*types.Volume{
{
Name: "volume",

View File

@ -6,11 +6,9 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"syscall"
@ -19,7 +17,6 @@ import (
"github.com/containerd/containerd/cio"
containertypes "github.com/docker/docker/api/types/container"
mounttypes "github.com/docker/docker/api/types/mount"
networktypes "github.com/docker/docker/api/types/network"
swarmtypes "github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/container/stream"
"github.com/docker/docker/daemon/exec"
@ -28,7 +25,6 @@ import (
"github.com/docker/docker/daemon/network"
"github.com/docker/docker/image"
"github.com/docker/docker/layer"
"github.com/docker/docker/opts"
"github.com/docker/docker/pkg/containerfs"
"github.com/docker/docker/pkg/idtools"
"github.com/docker/docker/pkg/ioutils"
@ -36,15 +32,9 @@ import (
"github.com/docker/docker/pkg/symlink"
"github.com/docker/docker/pkg/system"
"github.com/docker/docker/restartmanager"
"github.com/docker/docker/runconfig"
"github.com/docker/docker/volume"
volumemounts "github.com/docker/docker/volume/mounts"
"github.com/docker/go-connections/nat"
units "github.com/docker/go-units"
"github.com/docker/libnetwork"
"github.com/docker/libnetwork/netlabel"
"github.com/docker/libnetwork/options"
"github.com/docker/libnetwork/types"
"github.com/docker/go-units"
agentexec "github.com/docker/swarmkit/agent/exec"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
@ -52,11 +42,6 @@ import (
const configFileName = "config.v2.json"
var (
errInvalidEndpoint = errors.New("invalid endpoint while building port map info")
errInvalidNetwork = errors.New("invalid network settings while building port map info")
)
// ExitStatus provides exit reasons for a container.
type ExitStatus struct {
// The exit code with which the container exited.
@ -538,366 +523,6 @@ func (container *Container) InitDNSHostConfig() {
}
}
// GetEndpointInNetwork returns the container's endpoint to the provided network.
func (container *Container) GetEndpointInNetwork(n libnetwork.Network) (libnetwork.Endpoint, error) {
endpointName := strings.TrimPrefix(container.Name, "/")
return n.EndpointByName(endpointName)
}
func (container *Container) buildPortMapInfo(ep libnetwork.Endpoint) error {
if ep == nil {
return errInvalidEndpoint
}
networkSettings := container.NetworkSettings
if networkSettings == nil {
return errInvalidNetwork
}
if len(networkSettings.Ports) == 0 {
pm, err := getEndpointPortMapInfo(ep)
if err != nil {
return err
}
networkSettings.Ports = pm
}
return nil
}
func getEndpointPortMapInfo(ep libnetwork.Endpoint) (nat.PortMap, error) {
pm := nat.PortMap{}
driverInfo, err := ep.DriverInfo()
if err != nil {
return pm, err
}
if driverInfo == nil {
// It is not an error for epInfo to be nil
return pm, nil
}
if expData, ok := driverInfo[netlabel.ExposedPorts]; ok {
if exposedPorts, ok := expData.([]types.TransportPort); ok {
for _, tp := range exposedPorts {
natPort, err := nat.NewPort(tp.Proto.String(), strconv.Itoa(int(tp.Port)))
if err != nil {
return pm, fmt.Errorf("Error parsing Port value(%v):%v", tp.Port, err)
}
pm[natPort] = nil
}
}
}
mapData, ok := driverInfo[netlabel.PortMap]
if !ok {
return pm, nil
}
if portMapping, ok := mapData.([]types.PortBinding); ok {
for _, pp := range portMapping {
natPort, err := nat.NewPort(pp.Proto.String(), strconv.Itoa(int(pp.Port)))
if err != nil {
return pm, err
}
natBndg := nat.PortBinding{HostIP: pp.HostIP.String(), HostPort: strconv.Itoa(int(pp.HostPort))}
pm[natPort] = append(pm[natPort], natBndg)
}
}
return pm, nil
}
// GetSandboxPortMapInfo retrieves the current port-mapping programmed for the given sandbox
func GetSandboxPortMapInfo(sb libnetwork.Sandbox) nat.PortMap {
pm := nat.PortMap{}
if sb == nil {
return pm
}
for _, ep := range sb.Endpoints() {
pm, _ = getEndpointPortMapInfo(ep)
if len(pm) > 0 {
break
}
}
return pm
}
// BuildEndpointInfo sets endpoint-related fields on container.NetworkSettings based on the provided network and endpoint.
func (container *Container) BuildEndpointInfo(n libnetwork.Network, ep libnetwork.Endpoint) error {
if ep == nil {
return errInvalidEndpoint
}
networkSettings := container.NetworkSettings
if networkSettings == nil {
return errInvalidNetwork
}
epInfo := ep.Info()
if epInfo == nil {
// It is not an error to get an empty endpoint info
return nil
}
if _, ok := networkSettings.Networks[n.Name()]; !ok {
networkSettings.Networks[n.Name()] = &network.EndpointSettings{
EndpointSettings: &networktypes.EndpointSettings{},
}
}
networkSettings.Networks[n.Name()].NetworkID = n.ID()
networkSettings.Networks[n.Name()].EndpointID = ep.ID()
iface := epInfo.Iface()
if iface == nil {
return nil
}
if iface.MacAddress() != nil {
networkSettings.Networks[n.Name()].MacAddress = iface.MacAddress().String()
}
if iface.Address() != nil {
ones, _ := iface.Address().Mask.Size()
networkSettings.Networks[n.Name()].IPAddress = iface.Address().IP.String()
networkSettings.Networks[n.Name()].IPPrefixLen = ones
}
if iface.AddressIPv6() != nil && iface.AddressIPv6().IP.To16() != nil {
onesv6, _ := iface.AddressIPv6().Mask.Size()
networkSettings.Networks[n.Name()].GlobalIPv6Address = iface.AddressIPv6().IP.String()
networkSettings.Networks[n.Name()].GlobalIPv6PrefixLen = onesv6
}
return nil
}
type named interface {
Name() string
}
// UpdateJoinInfo updates network settings when container joins network n with endpoint ep.
func (container *Container) UpdateJoinInfo(n named, ep libnetwork.Endpoint) error {
if err := container.buildPortMapInfo(ep); err != nil {
return err
}
epInfo := ep.Info()
if epInfo == nil {
// It is not an error to get an empty endpoint info
return nil
}
if epInfo.Gateway() != nil {
container.NetworkSettings.Networks[n.Name()].Gateway = epInfo.Gateway().String()
}
if epInfo.GatewayIPv6().To16() != nil {
container.NetworkSettings.Networks[n.Name()].IPv6Gateway = epInfo.GatewayIPv6().String()
}
return nil
}
// UpdateSandboxNetworkSettings updates the sandbox ID and Key.
func (container *Container) UpdateSandboxNetworkSettings(sb libnetwork.Sandbox) error {
container.NetworkSettings.SandboxID = sb.ID()
container.NetworkSettings.SandboxKey = sb.Key()
return nil
}
// BuildJoinOptions builds endpoint Join options from a given network.
func (container *Container) BuildJoinOptions(n named) ([]libnetwork.EndpointOption, error) {
var joinOptions []libnetwork.EndpointOption
if epConfig, ok := container.NetworkSettings.Networks[n.Name()]; ok {
for _, str := range epConfig.Links {
name, alias, err := opts.ParseLink(str)
if err != nil {
return nil, err
}
joinOptions = append(joinOptions, libnetwork.CreateOptionAlias(name, alias))
}
for k, v := range epConfig.DriverOpts {
joinOptions = append(joinOptions, libnetwork.EndpointOptionGeneric(options.Generic{k: v}))
}
}
return joinOptions, nil
}
// BuildCreateEndpointOptions builds endpoint options from a given network.
func (container *Container) BuildCreateEndpointOptions(n libnetwork.Network, epConfig *networktypes.EndpointSettings, sb libnetwork.Sandbox, daemonDNS []string) ([]libnetwork.EndpointOption, error) {
var (
bindings = make(nat.PortMap)
pbList []types.PortBinding
exposeList []types.TransportPort
createOptions []libnetwork.EndpointOption
)
defaultNetName := runconfig.DefaultDaemonNetworkMode().NetworkName()
if (!container.EnableServiceDiscoveryOnDefaultNetwork() && n.Name() == defaultNetName) ||
container.NetworkSettings.IsAnonymousEndpoint {
createOptions = append(createOptions, libnetwork.CreateOptionAnonymous())
}
if epConfig != nil {
ipam := epConfig.IPAMConfig
if ipam != nil {
var (
ipList []net.IP
ip, ip6, linkip net.IP
)
for _, ips := range ipam.LinkLocalIPs {
if linkip = net.ParseIP(ips); linkip == nil && ips != "" {
return nil, errors.Errorf("Invalid link-local IP address: %s", ipam.LinkLocalIPs)
}
ipList = append(ipList, linkip)
}
if ip = net.ParseIP(ipam.IPv4Address); ip == nil && ipam.IPv4Address != "" {
return nil, errors.Errorf("Invalid IPv4 address: %s)", ipam.IPv4Address)
}
if ip6 = net.ParseIP(ipam.IPv6Address); ip6 == nil && ipam.IPv6Address != "" {
return nil, errors.Errorf("Invalid IPv6 address: %s)", ipam.IPv6Address)
}
createOptions = append(createOptions,
libnetwork.CreateOptionIpam(ip, ip6, ipList, nil))
}
for _, alias := range epConfig.Aliases {
createOptions = append(createOptions, libnetwork.CreateOptionMyAlias(alias))
}
for k, v := range epConfig.DriverOpts {
createOptions = append(createOptions, libnetwork.EndpointOptionGeneric(options.Generic{k: v}))
}
}
if container.NetworkSettings.Service != nil {
svcCfg := container.NetworkSettings.Service
var vip string
if svcCfg.VirtualAddresses[n.ID()] != nil {
vip = svcCfg.VirtualAddresses[n.ID()].IPv4
}
var portConfigs []*libnetwork.PortConfig
for _, portConfig := range svcCfg.ExposedPorts {
portConfigs = append(portConfigs, &libnetwork.PortConfig{
Name: portConfig.Name,
Protocol: libnetwork.PortConfig_Protocol(portConfig.Protocol),
TargetPort: portConfig.TargetPort,
PublishedPort: portConfig.PublishedPort,
})
}
createOptions = append(createOptions, libnetwork.CreateOptionService(svcCfg.Name, svcCfg.ID, net.ParseIP(vip), portConfigs, svcCfg.Aliases[n.ID()]))
}
if !containertypes.NetworkMode(n.Name()).IsUserDefined() {
createOptions = append(createOptions, libnetwork.CreateOptionDisableResolution())
}
// configs that are applicable only for the endpoint in the network
// to which container was connected to on docker run.
// Ideally all these network-specific endpoint configurations must be moved under
// container.NetworkSettings.Networks[n.Name()]
if n.Name() == container.HostConfig.NetworkMode.NetworkName() ||
(n.Name() == defaultNetName && container.HostConfig.NetworkMode.IsDefault()) {
if container.Config.MacAddress != "" {
mac, err := net.ParseMAC(container.Config.MacAddress)
if err != nil {
return nil, err
}
genericOption := options.Generic{
netlabel.MacAddress: mac,
}
createOptions = append(createOptions, libnetwork.EndpointOptionGeneric(genericOption))
}
}
// Port-mapping rules belong to the container & applicable only to non-internal networks
portmaps := GetSandboxPortMapInfo(sb)
if n.Info().Internal() || len(portmaps) > 0 {
return createOptions, nil
}
if container.HostConfig.PortBindings != nil {
for p, b := range container.HostConfig.PortBindings {
bindings[p] = []nat.PortBinding{}
for _, bb := range b {
bindings[p] = append(bindings[p], nat.PortBinding{
HostIP: bb.HostIP,
HostPort: bb.HostPort,
})
}
}
}
portSpecs := container.Config.ExposedPorts
ports := make([]nat.Port, len(portSpecs))
var i int
for p := range portSpecs {
ports[i] = p
i++
}
nat.SortPortMap(ports, bindings)
for _, port := range ports {
expose := types.TransportPort{}
expose.Proto = types.ParseProtocol(port.Proto())
expose.Port = uint16(port.Int())
exposeList = append(exposeList, expose)
pb := types.PortBinding{Port: expose.Port, Proto: expose.Proto}
binding := bindings[port]
for i := 0; i < len(binding); i++ {
pbCopy := pb.GetCopy()
newP, err := nat.NewPort(nat.SplitProtoPort(binding[i].HostPort))
var portStart, portEnd int
if err == nil {
portStart, portEnd, err = newP.Range()
}
if err != nil {
return nil, errors.Wrapf(err, "Error parsing HostPort value (%s)", binding[i].HostPort)
}
pbCopy.HostPort = uint16(portStart)
pbCopy.HostPortEnd = uint16(portEnd)
pbCopy.HostIP = net.ParseIP(binding[i].HostIP)
pbList = append(pbList, pbCopy)
}
if container.HostConfig.PublishAllPorts && len(binding) == 0 {
pbList = append(pbList, pb)
}
}
var dns []string
if len(container.HostConfig.DNS) > 0 {
dns = container.HostConfig.DNS
} else if len(daemonDNS) > 0 {
dns = daemonDNS
}
if len(dns) > 0 {
createOptions = append(createOptions,
libnetwork.CreateOptionDNS(dns))
}
createOptions = append(createOptions,
libnetwork.CreateOptionPortMapping(pbList),
libnetwork.CreateOptionExposedPorts(exposeList))
return createOptions, nil
}
// UpdateMonitor updates monitor configure for running container
func (container *Container) UpdateMonitor(restartPolicy containertypes.RestartPolicy) {
type policySetter interface {

View File

@ -1,9 +0,0 @@
package container // import "github.com/docker/docker/container"
import (
"golang.org/x/sys/unix"
)
func detachMounted(path string) error {
return unix.Unmount(path, unix.MNT_DETACH)
}

View File

@ -1,23 +0,0 @@
// +build freebsd
package container // import "github.com/docker/docker/container"
import (
"golang.org/x/sys/unix"
)
func detachMounted(path string) error {
// FreeBSD do not support the lazy unmount or MNT_DETACH feature.
// Therefore there are separate definitions for this.
return unix.Unmount(path, 0)
}
// SecretMounts returns the mounts for the secret path
func (container *Container) SecretMounts() []Mount {
return nil
}
// UnmountSecrets unmounts the fs for secrets
func (container *Container) UnmountSecrets() error {
return nil
}

View File

@ -1,4 +1,4 @@
// +build linux freebsd
// +build !windows
package container // import "github.com/docker/docker/container"
@ -380,7 +380,7 @@ func (container *Container) DetachAndUnmount(volumeEventLog func(name, action st
}
for _, mountPath := range mountPaths {
if err := detachMounted(mountPath); err != nil {
if err := mount.Unmount(mountPath); err != nil {
logrus.Warnf("%s unmountVolumes: Failed to do lazy umount fo volume '%s': %v", container.ID, mountPath, err)
}
}

View File

@ -398,7 +398,7 @@ func (c *containerConfig) hostConfig() *enginecontainer.HostConfig {
}
// This handles the case of volumes that are defined inside a service Mount
func (c *containerConfig) volumeCreateRequest(mount *api.Mount) *volumetypes.VolumesCreateBody {
func (c *containerConfig) volumeCreateRequest(mount *api.Mount) *volumetypes.VolumeCreateBody {
var (
driverName string
driverOpts map[string]string
@ -412,7 +412,7 @@ func (c *containerConfig) volumeCreateRequest(mount *api.Mount) *volumetypes.Vol
}
if mount.VolumeOptions != nil {
return &volumetypes.VolumesCreateBody{
return &volumetypes.VolumeCreateBody{
Name: mount.Source,
Driver: driverName,
DriverOpts: driverOpts,

View File

@ -31,7 +31,7 @@ var (
// ErrRootFSReadOnly is returned when a container
// rootfs is marked readonly.
ErrRootFSReadOnly = errors.New("container rootfs is marked read-only")
getPortMapInfo = container.GetSandboxPortMapInfo
getPortMapInfo = getSandboxPortMapInfo
)
func (daemon *Daemon) getDNSSearchSettings(container *container.Container) []string {
@ -288,7 +288,7 @@ func (daemon *Daemon) updateNetworkSettings(container *container.Container, n li
}
func (daemon *Daemon) updateEndpointNetworkSettings(container *container.Container, n libnetwork.Network, ep libnetwork.Endpoint) error {
if err := container.BuildEndpointInfo(n, ep); err != nil {
if err := buildEndpointInfo(container.NetworkSettings, n, ep); err != nil {
return err
}
@ -572,7 +572,7 @@ func (daemon *Daemon) allocateNetwork(container *container.Container) error {
if err != nil {
return err
}
container.UpdateSandboxNetworkSettings(sb)
updateSandboxNetworkSettings(container, sb)
defer func() {
if err != nil {
sb.Delete()
@ -740,7 +740,7 @@ func (daemon *Daemon) connectToNetwork(container *container.Container, idOrName
controller := daemon.netController
sb := daemon.getNetworkSandbox(container)
createOptions, err := container.BuildCreateEndpointOptions(n, endpointConfig, sb, daemon.configStore.DNS)
createOptions, err := buildCreateEndpointOptions(container, n, endpointConfig, sb, daemon.configStore.DNS)
if err != nil {
return err
}
@ -779,10 +779,10 @@ func (daemon *Daemon) connectToNetwork(container *container.Container, idOrName
return err
}
container.UpdateSandboxNetworkSettings(sb)
updateSandboxNetworkSettings(container, sb)
}
joinOptions, err := container.BuildJoinOptions(n)
joinOptions, err := buildJoinOptions(container.NetworkSettings, n)
if err != nil {
return err
}
@ -798,7 +798,7 @@ func (daemon *Daemon) connectToNetwork(container *container.Container, idOrName
}
}
if err := container.UpdateJoinInfo(n, ep); err != nil {
if err := updateJoinInfo(container.NetworkSettings, n, ep); err != nil {
return fmt.Errorf("Updating join info failed: %v", err)
}
@ -809,6 +809,37 @@ func (daemon *Daemon) connectToNetwork(container *container.Container, idOrName
return nil
}
func updateJoinInfo(networkSettings *network.Settings, n libnetwork.Network, ep libnetwork.Endpoint) error { // nolint: interfacer
if ep == nil {
return errors.New("invalid enppoint whhile building portmap info")
}
if networkSettings == nil {
return errors.New("invalid network settings while building port map info")
}
if len(networkSettings.Ports) == 0 {
pm, err := getEndpointPortMapInfo(ep)
if err != nil {
return err
}
networkSettings.Ports = pm
}
epInfo := ep.Info()
if epInfo == nil {
// It is not an error to get an empty endpoint info
return nil
}
if epInfo.Gateway() != nil {
networkSettings.Networks[n.Name()].Gateway = epInfo.Gateway().String()
}
if epInfo.GatewayIPv6().To16() != nil {
networkSettings.Networks[n.Name()].IPv6Gateway = epInfo.GatewayIPv6().String()
}
return nil
}
// ForceEndpointDelete deletes an endpoint from a network forcefully
func (daemon *Daemon) ForceEndpointDelete(name string, networkName string) error {
n, err := daemon.FindNetwork(networkName)
@ -1110,3 +1141,10 @@ func getNetworkID(name string, endpointSettings *networktypes.EndpointSettings)
}
return name
}
// updateSandboxNetworkSettings updates the sandbox ID and Key.
func updateSandboxNetworkSettings(c *container.Container, sb libnetwork.Sandbox) error {
c.NetworkSettings.SandboxID = sb.ID()
c.NetworkSettings.SandboxKey = sb.Key()
return nil
}

View File

@ -174,7 +174,7 @@ func (daemon *Daemon) initializeNetworkingPaths(container *container.Container,
continue
}
ep, err := nc.GetEndpointInNetwork(sn)
ep, err := getEndpointInNetwork(nc.Name, sn)
if err != nil {
continue
}

View File

@ -97,7 +97,7 @@ type LogFile struct {
type makeDecoderFunc func(rdr io.Reader) func() (*logger.Message, error)
//NewLogFile creates new LogFile
// NewLogFile creates new LogFile
func NewLogFile(logPath string, capacity int64, maxFiles int, compress bool, marshaller logger.MarshalFunc, decodeFunc makeDecoderFunc, perms os.FileMode) (*LogFile, error) {
log, err := os.OpenFile(logPath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, perms)
if err != nil {
@ -201,6 +201,13 @@ func rotate(name string, maxFiles int, compress bool) error {
if compress {
extension = ".gz"
}
lastFile := fmt.Sprintf("%s.%d%s", name, maxFiles-1, extension)
err := os.Remove(lastFile)
if err != nil && !os.IsNotExist(err) {
return errors.Wrap(err, "error removing oldest log file")
}
for i := maxFiles - 1; i > 1; i-- {
toPath := name + "." + strconv.Itoa(i) + extension
fromPath := name + "." + strconv.Itoa(i-1) + extension
@ -230,7 +237,7 @@ func compressFile(fileName string, lastTimestamp time.Time) {
}
}()
outFile, err := os.OpenFile(fileName+".gz", os.O_CREATE|os.O_RDWR, 0640)
outFile, err := os.OpenFile(fileName+".gz", os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0640)
if err != nil {
logrus.Errorf("Failed to open or create gzip log file: %v", err)
return
@ -251,7 +258,7 @@ func compressFile(fileName string, lastTimestamp time.Time) {
compressWriter.Header.Extra, err = json.Marshal(&extra)
if err != nil {
// Here log the error only and don't return since this is just an optimization.
logrus.Warningf("Failed to marshal JSON: %v", err)
logrus.Warningf("Failed to marshal gzip header as JSON: %v", err)
}
_, err = pools.Copy(compressWriter, file)
@ -281,6 +288,9 @@ func (w *LogFile) Close() error {
}
// ReadLogs decodes entries from log files and sends them the passed in watcher
//
// Note: Using the follow option can become inconsistent in cases with very frequent rotations and max log files is 1.
// TODO: Consider a different implementation which can effectively follow logs under frequent rotations.
func (w *LogFile) ReadLogs(config logger.ReadConfig, watcher *logger.LogWatcher) {
w.mu.RLock()
currentFile, err := os.Open(w.f.Name())
@ -364,7 +374,7 @@ func (w *LogFile) openRotatedFiles(config logger.ReadConfig) (files []*os.File,
f, err := os.Open(fmt.Sprintf("%s.%d", w.f.Name(), i-1))
if err != nil {
if !os.IsNotExist(err) {
return nil, err
return nil, errors.Wrap(err, "error opening rotated log file")
}
fileName := fmt.Sprintf("%s.%d.gz", w.f.Name(), i-1)
@ -377,8 +387,8 @@ func (w *LogFile) openRotatedFiles(config logger.ReadConfig) (files []*os.File,
})
if err != nil {
if !os.IsNotExist(err) {
return nil, err
if !os.IsNotExist(errors.Cause(err)) {
return nil, errors.Wrap(err, "error getting reference to decompressed log file")
}
continue
}
@ -399,13 +409,13 @@ func (w *LogFile) openRotatedFiles(config logger.ReadConfig) (files []*os.File,
func decompressfile(fileName, destFileName string, since time.Time) (*os.File, error) {
cf, err := os.Open(fileName)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "error opening file for decompression")
}
defer cf.Close()
rc, err := gzip.NewReader(cf)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "error making gzip reader for compressed log file")
}
defer rc.Close()
@ -418,17 +428,17 @@ func decompressfile(fileName, destFileName string, since time.Time) (*os.File, e
rs, err := os.OpenFile(destFileName, os.O_CREATE|os.O_RDWR, 0640)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "error creating file for copying decompressed log stream")
}
_, err = pools.Copy(rs, rc)
if err != nil {
rs.Close()
rErr := os.Remove(rs.Name())
if rErr != nil && os.IsNotExist(rErr) {
if rErr != nil && !os.IsNotExist(rErr) {
logrus.Errorf("Failed to remove the logfile %q: %v", rs.Name(), rErr)
}
return nil, err
return nil, errors.Wrap(err, "error while copying decompressed log stream to file")
}
return rs, nil
@ -461,7 +471,7 @@ func tailFile(f io.ReadSeeker, watcher *logger.LogWatcher, createDecoder makeDec
for {
msg, err := decodeLogLine()
if err != nil {
if err != io.EOF {
if errors.Cause(err) != io.EOF {
watcher.Err <- err
}
return
@ -569,7 +579,7 @@ func followLogs(f *os.File, logWatcher *logger.LogWatcher, notifyRotate chan int
}
handleDecodeErr := func(err error) error {
if err != io.EOF {
if errors.Cause(err) != io.EOF {
return err
}

View File

@ -12,7 +12,10 @@ import (
)
func openPluginStream(a *pluginAdapter) (io.WriteCloser, error) {
f, err := fifo.OpenFifo(context.Background(), a.fifoPath, unix.O_WRONLY|unix.O_CREAT|unix.O_NONBLOCK, 0700)
// Make sure to also open with read (in addition to write) to avoid borken pipe errors on plugin failure.
// It is up to the plugin to keep track of pipes that it should re-attach to, however.
// If the plugin doesn't open for reads, then the container will block once the pipe is full.
f, err := fifo.OpenFifo(context.Background(), a.fifoPath, unix.O_RDWR|unix.O_CREAT|unix.O_NONBLOCK, 0700)
if err != nil {
return nil, errors.Wrapf(err, "error creating i/o pipe for log plugin: %s", a.Name())
}

View File

@ -22,7 +22,7 @@ import (
//
// if it returns nil, the config channel will be active and return log
// messages until it runs out or the context is canceled.
func (daemon *Daemon) ContainerLogs(ctx context.Context, containerName string, config *types.ContainerLogsOptions) (<-chan *backend.LogMessage, bool, error) {
func (daemon *Daemon) ContainerLogs(ctx context.Context, containerName string, config *types.ContainerLogsOptions) (messages <-chan *backend.LogMessage, isTTY bool, retErr error) {
lg := logrus.WithFields(logrus.Fields{
"module": "daemon",
"method": "(*Daemon).ContainerLogs",
@ -51,8 +51,10 @@ func (daemon *Daemon) ContainerLogs(ctx context.Context, containerName string, c
}
if cLogCreated {
defer func() {
if err = cLog.Close(); err != nil {
logrus.Errorf("Error closing logger: %v", err)
if retErr != nil {
if err = cLog.Close(); err != nil {
logrus.Errorf("Error closing logger: %v", err)
}
}
}()
}
@ -101,6 +103,13 @@ func (daemon *Daemon) ContainerLogs(ctx context.Context, containerName string, c
// this goroutine functions as a shim between the logger and the caller.
messageChan := make(chan *backend.LogMessage, 1)
go func() {
if cLogCreated {
defer func() {
if err = cLog.Close(); err != nil {
logrus.Errorf("Error closing logger: %v", err)
}
}()
}
// set up some defers
defer logs.Close()

View File

@ -6,19 +6,27 @@ import (
"net"
"runtime"
"sort"
"strconv"
"strings"
"sync"
"github.com/docker/docker/api/types"
containertypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/container"
clustertypes "github.com/docker/docker/daemon/cluster/provider"
internalnetwork "github.com/docker/docker/daemon/network"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/opts"
"github.com/docker/docker/pkg/plugingetter"
"github.com/docker/docker/runconfig"
"github.com/docker/go-connections/nat"
"github.com/docker/libnetwork"
lncluster "github.com/docker/libnetwork/cluster"
"github.com/docker/libnetwork/driverapi"
"github.com/docker/libnetwork/ipamapi"
"github.com/docker/libnetwork/netlabel"
"github.com/docker/libnetwork/options"
networktypes "github.com/docker/libnetwork/types"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
@ -48,7 +56,7 @@ func (daemon *Daemon) NetworkControllerEnabled() bool {
func (daemon *Daemon) FindNetwork(term string) (libnetwork.Network, error) {
listByFullName := []libnetwork.Network{}
listByPartialID := []libnetwork.Network{}
for _, nw := range daemon.GetNetworks() {
for _, nw := range daemon.getAllNetworks() {
if nw.ID() == term {
return nw, nil
}
@ -575,7 +583,7 @@ func (daemon *Daemon) GetNetworks() []libnetwork.Network {
// clearAttachableNetworks removes the attachable networks
// after disconnecting any connected container
func (daemon *Daemon) clearAttachableNetworks() {
for _, n := range daemon.GetNetworks() {
for _, n := range daemon.getAllNetworks() {
if !n.Info().Attachable() {
continue
}
@ -599,3 +607,312 @@ func (daemon *Daemon) clearAttachableNetworks() {
}
}
}
// buildCreateEndpointOptions builds endpoint options from a given network.
func buildCreateEndpointOptions(c *container.Container, n libnetwork.Network, epConfig *network.EndpointSettings, sb libnetwork.Sandbox, daemonDNS []string) ([]libnetwork.EndpointOption, error) {
var (
bindings = make(nat.PortMap)
pbList []networktypes.PortBinding
exposeList []networktypes.TransportPort
createOptions []libnetwork.EndpointOption
)
defaultNetName := runconfig.DefaultDaemonNetworkMode().NetworkName()
if (!c.EnableServiceDiscoveryOnDefaultNetwork() && n.Name() == defaultNetName) ||
c.NetworkSettings.IsAnonymousEndpoint {
createOptions = append(createOptions, libnetwork.CreateOptionAnonymous())
}
if epConfig != nil {
ipam := epConfig.IPAMConfig
if ipam != nil {
var (
ipList []net.IP
ip, ip6, linkip net.IP
)
for _, ips := range ipam.LinkLocalIPs {
if linkip = net.ParseIP(ips); linkip == nil && ips != "" {
return nil, errors.Errorf("Invalid link-local IP address: %s", ipam.LinkLocalIPs)
}
ipList = append(ipList, linkip)
}
if ip = net.ParseIP(ipam.IPv4Address); ip == nil && ipam.IPv4Address != "" {
return nil, errors.Errorf("Invalid IPv4 address: %s)", ipam.IPv4Address)
}
if ip6 = net.ParseIP(ipam.IPv6Address); ip6 == nil && ipam.IPv6Address != "" {
return nil, errors.Errorf("Invalid IPv6 address: %s)", ipam.IPv6Address)
}
createOptions = append(createOptions,
libnetwork.CreateOptionIpam(ip, ip6, ipList, nil))
}
for _, alias := range epConfig.Aliases {
createOptions = append(createOptions, libnetwork.CreateOptionMyAlias(alias))
}
for k, v := range epConfig.DriverOpts {
createOptions = append(createOptions, libnetwork.EndpointOptionGeneric(options.Generic{k: v}))
}
}
if c.NetworkSettings.Service != nil {
svcCfg := c.NetworkSettings.Service
var vip string
if svcCfg.VirtualAddresses[n.ID()] != nil {
vip = svcCfg.VirtualAddresses[n.ID()].IPv4
}
var portConfigs []*libnetwork.PortConfig
for _, portConfig := range svcCfg.ExposedPorts {
portConfigs = append(portConfigs, &libnetwork.PortConfig{
Name: portConfig.Name,
Protocol: libnetwork.PortConfig_Protocol(portConfig.Protocol),
TargetPort: portConfig.TargetPort,
PublishedPort: portConfig.PublishedPort,
})
}
createOptions = append(createOptions, libnetwork.CreateOptionService(svcCfg.Name, svcCfg.ID, net.ParseIP(vip), portConfigs, svcCfg.Aliases[n.ID()]))
}
if !containertypes.NetworkMode(n.Name()).IsUserDefined() {
createOptions = append(createOptions, libnetwork.CreateOptionDisableResolution())
}
// configs that are applicable only for the endpoint in the network
// to which container was connected to on docker run.
// Ideally all these network-specific endpoint configurations must be moved under
// container.NetworkSettings.Networks[n.Name()]
if n.Name() == c.HostConfig.NetworkMode.NetworkName() ||
(n.Name() == defaultNetName && c.HostConfig.NetworkMode.IsDefault()) {
if c.Config.MacAddress != "" {
mac, err := net.ParseMAC(c.Config.MacAddress)
if err != nil {
return nil, err
}
genericOption := options.Generic{
netlabel.MacAddress: mac,
}
createOptions = append(createOptions, libnetwork.EndpointOptionGeneric(genericOption))
}
}
// Port-mapping rules belong to the container & applicable only to non-internal networks
portmaps := getSandboxPortMapInfo(sb)
if n.Info().Internal() || len(portmaps) > 0 {
return createOptions, nil
}
if c.HostConfig.PortBindings != nil {
for p, b := range c.HostConfig.PortBindings {
bindings[p] = []nat.PortBinding{}
for _, bb := range b {
bindings[p] = append(bindings[p], nat.PortBinding{
HostIP: bb.HostIP,
HostPort: bb.HostPort,
})
}
}
}
portSpecs := c.Config.ExposedPorts
ports := make([]nat.Port, len(portSpecs))
var i int
for p := range portSpecs {
ports[i] = p
i++
}
nat.SortPortMap(ports, bindings)
for _, port := range ports {
expose := networktypes.TransportPort{}
expose.Proto = networktypes.ParseProtocol(port.Proto())
expose.Port = uint16(port.Int())
exposeList = append(exposeList, expose)
pb := networktypes.PortBinding{Port: expose.Port, Proto: expose.Proto}
binding := bindings[port]
for i := 0; i < len(binding); i++ {
pbCopy := pb.GetCopy()
newP, err := nat.NewPort(nat.SplitProtoPort(binding[i].HostPort))
var portStart, portEnd int
if err == nil {
portStart, portEnd, err = newP.Range()
}
if err != nil {
return nil, errors.Wrapf(err, "Error parsing HostPort value (%s)", binding[i].HostPort)
}
pbCopy.HostPort = uint16(portStart)
pbCopy.HostPortEnd = uint16(portEnd)
pbCopy.HostIP = net.ParseIP(binding[i].HostIP)
pbList = append(pbList, pbCopy)
}
if c.HostConfig.PublishAllPorts && len(binding) == 0 {
pbList = append(pbList, pb)
}
}
var dns []string
if len(c.HostConfig.DNS) > 0 {
dns = c.HostConfig.DNS
} else if len(daemonDNS) > 0 {
dns = daemonDNS
}
if len(dns) > 0 {
createOptions = append(createOptions,
libnetwork.CreateOptionDNS(dns))
}
createOptions = append(createOptions,
libnetwork.CreateOptionPortMapping(pbList),
libnetwork.CreateOptionExposedPorts(exposeList))
return createOptions, nil
}
// getEndpointInNetwork returns the container's endpoint to the provided network.
func getEndpointInNetwork(name string, n libnetwork.Network) (libnetwork.Endpoint, error) {
endpointName := strings.TrimPrefix(name, "/")
return n.EndpointByName(endpointName)
}
// getSandboxPortMapInfo retrieves the current port-mapping programmed for the given sandbox
func getSandboxPortMapInfo(sb libnetwork.Sandbox) nat.PortMap {
pm := nat.PortMap{}
if sb == nil {
return pm
}
for _, ep := range sb.Endpoints() {
pm, _ = getEndpointPortMapInfo(ep)
if len(pm) > 0 {
break
}
}
return pm
}
func getEndpointPortMapInfo(ep libnetwork.Endpoint) (nat.PortMap, error) {
pm := nat.PortMap{}
driverInfo, err := ep.DriverInfo()
if err != nil {
return pm, err
}
if driverInfo == nil {
// It is not an error for epInfo to be nil
return pm, nil
}
if expData, ok := driverInfo[netlabel.ExposedPorts]; ok {
if exposedPorts, ok := expData.([]networktypes.TransportPort); ok {
for _, tp := range exposedPorts {
natPort, err := nat.NewPort(tp.Proto.String(), strconv.Itoa(int(tp.Port)))
if err != nil {
return pm, fmt.Errorf("Error parsing Port value(%v):%v", tp.Port, err)
}
pm[natPort] = nil
}
}
}
mapData, ok := driverInfo[netlabel.PortMap]
if !ok {
return pm, nil
}
if portMapping, ok := mapData.([]networktypes.PortBinding); ok {
for _, pp := range portMapping {
natPort, err := nat.NewPort(pp.Proto.String(), strconv.Itoa(int(pp.Port)))
if err != nil {
return pm, err
}
natBndg := nat.PortBinding{HostIP: pp.HostIP.String(), HostPort: strconv.Itoa(int(pp.HostPort))}
pm[natPort] = append(pm[natPort], natBndg)
}
}
return pm, nil
}
// buildEndpointInfo sets endpoint-related fields on container.NetworkSettings based on the provided network and endpoint.
func buildEndpointInfo(networkSettings *internalnetwork.Settings, n libnetwork.Network, ep libnetwork.Endpoint) error {
if ep == nil {
return errors.New("endpoint cannot be nil")
}
if networkSettings == nil {
return errors.New("network cannot be nil")
}
epInfo := ep.Info()
if epInfo == nil {
// It is not an error to get an empty endpoint info
return nil
}
if _, ok := networkSettings.Networks[n.Name()]; !ok {
networkSettings.Networks[n.Name()] = &internalnetwork.EndpointSettings{
EndpointSettings: &network.EndpointSettings{},
}
}
networkSettings.Networks[n.Name()].NetworkID = n.ID()
networkSettings.Networks[n.Name()].EndpointID = ep.ID()
iface := epInfo.Iface()
if iface == nil {
return nil
}
if iface.MacAddress() != nil {
networkSettings.Networks[n.Name()].MacAddress = iface.MacAddress().String()
}
if iface.Address() != nil {
ones, _ := iface.Address().Mask.Size()
networkSettings.Networks[n.Name()].IPAddress = iface.Address().IP.String()
networkSettings.Networks[n.Name()].IPPrefixLen = ones
}
if iface.AddressIPv6() != nil && iface.AddressIPv6().IP.To16() != nil {
onesv6, _ := iface.AddressIPv6().Mask.Size()
networkSettings.Networks[n.Name()].GlobalIPv6Address = iface.AddressIPv6().IP.String()
networkSettings.Networks[n.Name()].GlobalIPv6PrefixLen = onesv6
}
return nil
}
// buildJoinOptions builds endpoint Join options from a given network.
func buildJoinOptions(networkSettings *internalnetwork.Settings, n interface {
Name() string
}) ([]libnetwork.EndpointOption, error) {
var joinOptions []libnetwork.EndpointOption
if epConfig, ok := networkSettings.Networks[n.Name()]; ok {
for _, str := range epConfig.Links {
name, alias, err := opts.ParseLink(str)
if err != nil {
return nil, err
}
joinOptions = append(joinOptions, libnetwork.CreateOptionAlias(name, alias))
}
for k, v := range epConfig.DriverOpts {
joinOptions = append(joinOptions, libnetwork.EndpointOptionGeneric(options.Generic{k: v}))
}
}
return joinOptions, nil
}

View File

@ -156,7 +156,7 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
continue
}
ep, err := c.GetEndpointInNetwork(sn)
ep, err := getEndpointInNetwork(c.Name, sn)
if err != nil {
continue
}

View File

@ -23,5 +23,5 @@ swagger generate operation -f api/swagger.yaml \
-n ContainerUpdate \
-n ContainerWait \
-n ImageHistory \
-n VolumesCreate \
-n VolumesList
-n VolumeCreate \
-n VolumeList

View File

@ -41,7 +41,7 @@ func createTar(data map[string][]byte) (io.Reader, error) {
// which is attached to the container.
func createVolumeWithData(cli *client.Client, volumeName string, data map[string][]byte, image string) error {
_, err := cli.VolumeCreate(context.Background(),
volume.VolumesCreateBody{
volume.VolumeCreateBody{
Driver: "local",
Name: volumeName,
})

View File

@ -6,6 +6,7 @@
".*\\.pb\\.go",
"dockerversion/version_autogen.go",
"api/types/container/container_.*",
"api/types/volume/volume_.*",
"integration-cli/"
],
"Skip": ["integration-cli/"],

View File

@ -223,9 +223,7 @@ whitespace. It has been added to this example for clarity.
container using the image. This field can be <code>null</code>, in
which case any execution parameters should be specified at creation of
the container.
<h4>Container RunConfig Field Descriptions</h4>
<dl>
<dt>
User <code>string</code>
@ -234,9 +232,7 @@ whitespace. It has been added to this example for clarity.
<p>The username or UID which the process in the container should
run as. This acts as a default value to use when the value is
not specified when creating a container.</p>
<p>All of the following are valid:</p>
<ul>
<li><code>user</code></li>
<li><code>uid</code></li>
@ -245,7 +241,6 @@ whitespace. It has been added to this example for clarity.
<li><code>uid:group</code></li>
<li><code>user:gid</code></li>
</ul>
<p>If <code>group</code>/<code>gid</code> is not specified, the
default group and supplementary groups of the given
<code>user</code>/<code>uid</code> in <code>/etc/passwd</code>
@ -284,13 +279,11 @@ whitespace. It has been added to this example for clarity.
<code>map[string]struct{}</code> and is represented in JSON as
an object mapping its keys to an empty object. Here is an
example:
<pre>{
"8080": {},
"53/udp": {},
"2356/tcp": {}
}</pre>
Its keys can be in the format of:
<ul>
<li>
@ -304,10 +297,8 @@ whitespace. It has been added to this example for clarity.
</li>
</ul>
with the default protocol being <code>"tcp"</code> if not
specified.
These values act as defaults and are merged with any specified
when creating a container.
specified. These values act as defaults and are merged with any
specified when creating a container.
</dd>
<dt>
Env <code>array of strings</code>
@ -367,7 +358,6 @@ whitespace. It has been added to this example for clarity.
The rootfs key references the layer content addresses used by the
image. This makes the image config hash depend on the filesystem hash.
rootfs has two subkeys:
<ul>
<li>
<code>type</code> is usually set to <code>layers</code>.
@ -376,10 +366,7 @@ whitespace. It has been added to this example for clarity.
<code>diff_ids</code> is an array of layer content hashes (<code>DiffIDs</code>), in order from bottom-most to top-most.
</li>
</ul>
Here is an example rootfs section:
<pre>"rootfs": {
"diff_ids": [
"sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1",
@ -396,7 +383,6 @@ whitespace. It has been added to this example for clarity.
<code>history</code> is an array of objects describing the history of
each layer. The array is ordered from bottom-most layer to top-most
layer. The object has the following fields.
<ul>
<li>
<code>created</code>: Creation time, expressed as a ISO-8601 formatted

View File

@ -223,9 +223,7 @@ whitespace. It has been added to this example for clarity.
container using the image. This field can be <code>null</code>, in
which case any execution parameters should be specified at creation of
the container.
<h4>Container RunConfig Field Descriptions</h4>
<dl>
<dt>
User <code>string</code>
@ -234,9 +232,7 @@ whitespace. It has been added to this example for clarity.
<p>The username or UID which the process in the container should
run as. This acts as a default value to use when the value is
not specified when creating a container.</p>
<p>All of the following are valid:</p>
<ul>
<li><code>user</code></li>
<li><code>uid</code></li>
@ -245,7 +241,6 @@ whitespace. It has been added to this example for clarity.
<li><code>uid:group</code></li>
<li><code>user:gid</code></li>
</ul>
<p>If <code>group</code>/<code>gid</code> is not specified, the
default group and supplementary groups of the given
<code>user</code>/<code>uid</code> in <code>/etc/passwd</code>
@ -284,13 +279,11 @@ whitespace. It has been added to this example for clarity.
<code>map[string]struct{}</code> and is represented in JSON as
an object mapping its keys to an empty object. Here is an
example:
<pre>{
"8080": {},
"53/udp": {},
"2356/tcp": {}
}</pre>
Its keys can be in the format of:
<ul>
<li>
@ -304,10 +297,8 @@ whitespace. It has been added to this example for clarity.
</li>
</ul>
with the default protocol being <code>"tcp"</code> if not
specified.
These values act as defaults and are merged with any specified
when creating a container.
specified. These values act as defaults and are merged with
any specified when creating a container.
</dd>
<dt>
Env <code>array of strings</code>
@ -364,7 +355,6 @@ whitespace. It has been added to this example for clarity.
<li><code>["CMD", arg1, arg2, ...]</code> : exec arguments directly</li>
<li><code>["CMD-SHELL", command]</code> : run command with system's default shell</li>
</ul>
The test command should exit with a status of 0 if the container is healthy,
or with 1 if it is unhealthy.
</dd>
@ -387,12 +377,10 @@ whitespace. It has been added to this example for clarity.
The number of consecutive failures needed to consider a container as unhealthy.
</dd>
</dl>
In each case, the field can be omitted to indicate that the
value should be inherited from the base layer.
These values act as defaults and are merged with any specified
when creating a container.
value should be inherited from the base layer. These values act
as defaults and are merged with any specified when creating a
container.
</dd>
<dt>
Volumes <code>struct</code>
@ -426,7 +414,6 @@ whitespace. It has been added to this example for clarity.
The rootfs key references the layer content addresses used by the
image. This makes the image config hash depend on the filesystem hash.
rootfs has two subkeys:
<ul>
<li>
<code>type</code> is usually set to <code>layers</code>.
@ -435,10 +422,7 @@ whitespace. It has been added to this example for clarity.
<code>diff_ids</code> is an array of layer content hashes (<code>DiffIDs</code>), in order from bottom-most to top-most.
</li>
</ul>
Here is an example rootfs section:
<pre>"rootfs": {
"diff_ids": [
"sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1",
@ -455,7 +439,6 @@ whitespace. It has been added to this example for clarity.
<code>history</code> is an array of objects describing the history of
each layer. The array is ordered from bottom-most layer to top-most
layer. The object has the following fields.
<ul>
<li>
<code>created</code>: Creation time, expressed as a ISO-8601 formatted
@ -478,9 +461,7 @@ whitespace. It has been added to this example for clarity.
filesystem).
</li>
</ul>
Here is an example history section:
Here is an example history section:
<pre>"history": [
{
"created": "2015-10-31T22:22:54.690851953Z",

View File

@ -17,12 +17,10 @@ This specification uses the following terms:
<dd>
Images are composed of <i>layers</i>. <i>Image layer</i> is a general
term which may be used to refer to one or both of the following:
<ol>
<li>The metadata for the layer, described in the JSON format.</li>
<li>The filesystem changes described by a layer.</li>
</ol>
To refer to the former you may use the term <i>Layer JSON</i> or
<i>Layer Metadata</i>. To refer to the latter you may use the term
<i>Image Filesystem Changeset</i> or <i>Image Diff</i>.
@ -244,9 +242,7 @@ Here is an example image JSON file:
container using the image. This field can be <code>null</code>, in
which case any execution parameters should be specified at creation of
the container.
<h4>Container RunConfig Field Descriptions</h4>
<dl>
<dt>
User <code>string</code>
@ -255,9 +251,7 @@ Here is an example image JSON file:
<p>The username or UID which the process in the container should
run as. This acts as a default value to use when the value is
not specified when creating a container.</p>
<p>All of the following are valid:</p>
<ul>
<li><code>user</code></li>
<li><code>uid</code></li>
@ -266,7 +260,6 @@ Here is an example image JSON file:
<li><code>uid:group</code></li>
<li><code>user:gid</code></li>
</ul>
<p>If <code>group</code>/<code>gid</code> is not specified, the
default group and supplementary groups of the given
<code>user</code>/<code>uid</code> in <code>/etc/passwd</code>
@ -305,13 +298,11 @@ Here is an example image JSON file:
<code>map[string]struct{}</code> and is represented in JSON as
an object mapping its keys to an empty object. Here is an
example:
<pre>{
"8080": {},
"53/udp": {},
"2356/tcp": {}
}</pre>
Its keys can be in the format of:
<ul>
<li>
@ -325,9 +316,7 @@ Here is an example image JSON file:
</li>
</ul>
with the default protocol being <code>"tcp"</code> if not
specified.
These values act as defaults and are merged with any specified
specified. These values act as defaults and are merged with any specified
when creating a container.
</dd>
<dt>
@ -502,21 +491,21 @@ For example, here's what the full archive of `library/busybox` is (displayed in
```
.
├── 5785b62b697b99a5af6cd5d0aabc804d5748abbb6d3d07da5d1d3795f2dcc83e
   ├── VERSION
   ├── json
   └── layer.tar
├── VERSION
├── json
└── layer.tar
├── a7b8b41220991bfc754d7ad445ad27b7f272ab8b4a2c175b9512b97471d02a8a
   ├── VERSION
   ├── json
   └── layer.tar
├── VERSION
├── json
└── layer.tar
├── a936027c5ca8bf8f517923169a233e391cbb38469a75de8383b5228dc2d26ceb
   ├── VERSION
   ├── json
   └── layer.tar
├── VERSION
├── json
└── layer.tar
├── f60c56784b832dd990022afc120b8136ab3da9528094752ae13fe63a2d28dc8c
   ├── VERSION
   ├── json
   └── layer.tar
├── VERSION
├── json
└── layer.tar
└── repositories
```

View File

@ -116,3 +116,21 @@ func WithIPv6(network, ip string) func(*TestContainerConfig) {
c.NetworkingConfig.EndpointsConfig[network].IPAMConfig.IPv6Address = ip
}
}
// WithLogDriver sets the log driver to use for the container
func WithLogDriver(driver string) func(*TestContainerConfig) {
return func(c *TestContainerConfig) {
if c.HostConfig == nil {
c.HostConfig = &containertypes.HostConfig{}
}
c.HostConfig.LogConfig.Type = driver
}
}
// WithAutoRemove sets the container to be removed on exit
func WithAutoRemove(c *TestContainerConfig) {
if c.HostConfig == nil {
c.HostConfig = &containertypes.HostConfig{}
}
c.HostConfig.AutoRemove = true
}

View File

@ -77,7 +77,7 @@ func TestAuthZPluginV2Disable(t *testing.T) {
d.Restart(t, "--authorization-plugin="+authzPluginNameWithTag)
d.LoadBusybox(t)
_, err = client.VolumeCreate(context.Background(), volumetypes.VolumesCreateBody{Driver: "local"})
_, err = client.VolumeCreate(context.Background(), volumetypes.VolumeCreateBody{Driver: "local"})
assert.Assert(t, err != nil)
assert.Assert(t, strings.Contains(err.Error(), fmt.Sprintf("Error response from daemon: plugin %s failed with error:", authzPluginNameWithTag)))
@ -86,7 +86,7 @@ func TestAuthZPluginV2Disable(t *testing.T) {
assert.NilError(t, err)
// now test to see if the docker api works.
_, err = client.VolumeCreate(context.Background(), volumetypes.VolumesCreateBody{Driver: "local"})
_, err = client.VolumeCreate(context.Background(), volumetypes.VolumeCreateBody{Driver: "local"})
assert.NilError(t, err)
}
@ -104,7 +104,7 @@ func TestAuthZPluginV2RejectVolumeRequests(t *testing.T) {
// restart the daemon with the plugin
d.Restart(t, "--authorization-plugin="+authzPluginNameWithTag)
_, err = client.VolumeCreate(context.Background(), volumetypes.VolumesCreateBody{Driver: "local"})
_, err = client.VolumeCreate(context.Background(), volumetypes.VolumeCreateBody{Driver: "local"})
assert.Assert(t, err != nil)
assert.Assert(t, strings.Contains(err.Error(), fmt.Sprintf("Error response from daemon: plugin %s failed with error:", authzPluginNameWithTag)))

View File

@ -0,0 +1,48 @@
package main
import (
"encoding/json"
"fmt"
"net"
"net/http"
"os"
)
type start struct {
File string
}
func main() {
l, err := net.Listen("unix", "/run/docker/plugins/plugin.sock")
if err != nil {
panic(err)
}
mux := http.NewServeMux()
mux.HandleFunc("/LogDriver.StartLogging", func(w http.ResponseWriter, req *http.Request) {
startReq := &start{}
if err := json.NewDecoder(req.Body).Decode(startReq); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
f, err := os.OpenFile(startReq.File, os.O_RDONLY, 0600)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Close the file immediately, this allows us to test what happens in the daemon when the plugin has closed the
// file or, for example, the plugin has crashed.
f.Close()
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, `{}`)
})
server := http.Server{
Addr: l.Addr().String(),
Handler: mux,
}
server.Serve(l)
}

View File

@ -0,0 +1 @@
package main

View File

@ -0,0 +1,77 @@
package logging
import (
"bufio"
"context"
"os"
"strings"
"testing"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/integration/internal/container"
"github.com/docker/docker/internal/test/daemon"
"github.com/gotestyourself/gotestyourself/assert"
)
func TestContinueAfterPluginCrash(t *testing.T) {
t.Parallel()
d := daemon.New(t)
d.StartWithBusybox(t, "--iptables=false", "--init")
defer d.Stop(t)
client := d.NewClientT(t)
createPlugin(t, client, "test", "close_on_start", asLogDriver)
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
assert.Assert(t, client.PluginEnable(ctx, "test", types.PluginEnableOptions{Timeout: 30}))
cancel()
defer client.PluginRemove(context.Background(), "test", types.PluginRemoveOptions{Force: true})
ctx, cancel = context.WithTimeout(context.Background(), 60*time.Second)
id := container.Run(t, ctx, client,
container.WithAutoRemove,
container.WithLogDriver("test"),
container.WithCmd(
"/bin/sh", "-c", "while true; do sleep 1; echo hello; done",
),
)
cancel()
defer client.ContainerRemove(context.Background(), id, types.ContainerRemoveOptions{Force: true})
// Attach to the container to make sure it's written a few times to stdout
attach, err := client.ContainerAttach(context.Background(), id, types.ContainerAttachOptions{Stream: true, Stdout: true})
assert.Assert(t, err)
chErr := make(chan error)
go func() {
defer close(chErr)
rdr := bufio.NewReader(attach.Reader)
for i := 0; i < 5; i++ {
_, _, err := rdr.ReadLine()
if err != nil {
chErr <- err
return
}
}
}()
select {
case err := <-chErr:
assert.Assert(t, err)
case <-time.After(60 * time.Second):
t.Fatal("timeout waiting for container i/o")
}
// check daemon logs for "broken pipe"
// TODO(@cpuguy83): This is horribly hacky but is the only way to really test this case right now.
// It would be nice if there was a way to know that a broken pipe has occurred without looking through the logs.
log, err := os.Open(d.LogFileName())
assert.Assert(t, err)
scanner := bufio.NewScanner(log)
for scanner.Scan() {
assert.Assert(t, !strings.Contains(scanner.Text(), "broken pipe"))
}
}

View File

@ -34,15 +34,14 @@ func TestCreateServiceMultipleTimes(t *testing.T) {
overlayID := netResp.ID
var instances uint64 = 4
serviceSpec := swarmServiceSpec("TestService", instances)
serviceSpec.TaskTemplate.Networks = append(serviceSpec.TaskTemplate.Networks, swarmtypes.NetworkAttachmentConfig{Target: overlayName})
serviceResp, err := client.ServiceCreate(context.Background(), serviceSpec, types.ServiceCreateOptions{
QueryRegistry: false,
})
assert.NilError(t, err)
serviceSpec := []swarm.ServiceSpecOpt{
swarm.ServiceWithReplicas(instances),
swarm.ServiceWithName("TestService"),
swarm.ServiceWithNetwork(overlayName),
}
serviceID := serviceResp.ID
serviceID := swarm.CreateService(t, d, serviceSpec...)
poll.WaitOn(t, serviceRunningTasksCount(client, serviceID, instances), swarm.ServicePoll)
_, _, err = client.ServiceInspectWithRaw(context.Background(), serviceID, types.ServiceInspectOptions{})
@ -54,12 +53,7 @@ func TestCreateServiceMultipleTimes(t *testing.T) {
poll.WaitOn(t, serviceIsRemoved(client, serviceID), swarm.ServicePoll)
poll.WaitOn(t, noTasks(client), swarm.ServicePoll)
serviceResp, err = client.ServiceCreate(context.Background(), serviceSpec, types.ServiceCreateOptions{
QueryRegistry: false,
})
assert.NilError(t, err)
serviceID2 := serviceResp.ID
serviceID2 := swarm.CreateService(t, d, serviceSpec...)
poll.WaitOn(t, serviceRunningTasksCount(client, serviceID2, instances), swarm.ServicePoll)
err = client.ServiceRemove(context.Background(), serviceID2)
@ -100,25 +94,25 @@ func TestCreateWithDuplicateNetworkNames(t *testing.T) {
// Create Service with the same name
var instances uint64 = 1
serviceSpec := swarmServiceSpec("top", instances)
serviceSpec.TaskTemplate.Networks = append(serviceSpec.TaskTemplate.Networks, swarmtypes.NetworkAttachmentConfig{Target: name})
serviceID := swarm.CreateService(t, d,
swarm.ServiceWithReplicas(instances),
swarm.ServiceWithName("top"),
swarm.ServiceWithNetwork(name),
)
service, err := client.ServiceCreate(context.Background(), serviceSpec, types.ServiceCreateOptions{})
assert.NilError(t, err)
poll.WaitOn(t, serviceRunningTasksCount(client, serviceID, instances), swarm.ServicePoll)
poll.WaitOn(t, serviceRunningTasksCount(client, service.ID, instances), swarm.ServicePoll)
resp, _, err := client.ServiceInspectWithRaw(context.Background(), service.ID, types.ServiceInspectOptions{})
resp, _, err := client.ServiceInspectWithRaw(context.Background(), serviceID, types.ServiceInspectOptions{})
assert.NilError(t, err)
assert.Check(t, is.Equal(n3.ID, resp.Spec.TaskTemplate.Networks[0].Target))
// Remove Service
err = client.ServiceRemove(context.Background(), service.ID)
err = client.ServiceRemove(context.Background(), serviceID)
assert.NilError(t, err)
// Make sure task has been destroyed.
poll.WaitOn(t, serviceIsRemoved(client, service.ID), swarm.ServicePoll)
poll.WaitOn(t, serviceIsRemoved(client, serviceID), swarm.ServicePoll)
// Remove networks
err = client.NetworkRemove(context.Background(), n3.ID)
@ -153,44 +147,26 @@ func TestCreateServiceSecretFileMode(t *testing.T) {
assert.NilError(t, err)
var instances uint64 = 1
serviceSpec := swarmtypes.ServiceSpec{
Annotations: swarmtypes.Annotations{
Name: "TestService",
},
TaskTemplate: swarmtypes.TaskSpec{
ContainerSpec: &swarmtypes.ContainerSpec{
Image: "busybox:latest",
Command: []string{"/bin/sh", "-c", "ls -l /etc/secret || /bin/top"},
Secrets: []*swarmtypes.SecretReference{
{
File: &swarmtypes.SecretReferenceFileTarget{
Name: "/etc/secret",
UID: "0",
GID: "0",
Mode: 0777,
},
SecretID: secretResp.ID,
SecretName: "TestSecret",
},
},
serviceID := swarm.CreateService(t, d,
swarm.ServiceWithReplicas(instances),
swarm.ServiceWithName("TestService"),
swarm.ServiceWithCommand([]string{"/bin/sh", "-c", "ls -l /etc/secret || /bin/top"}),
swarm.ServiceWithSecret(&swarmtypes.SecretReference{
File: &swarmtypes.SecretReferenceFileTarget{
Name: "/etc/secret",
UID: "0",
GID: "0",
Mode: 0777,
},
},
Mode: swarmtypes.ServiceMode{
Replicated: &swarmtypes.ReplicatedService{
Replicas: &instances,
},
},
}
SecretID: secretResp.ID,
SecretName: "TestSecret",
}),
)
serviceResp, err := client.ServiceCreate(ctx, serviceSpec, types.ServiceCreateOptions{
QueryRegistry: false,
})
assert.NilError(t, err)
poll.WaitOn(t, serviceRunningTasksCount(client, serviceResp.ID, instances), swarm.ServicePoll)
poll.WaitOn(t, serviceRunningTasksCount(client, serviceID, instances), swarm.ServicePoll)
filter := filters.NewArgs()
filter.Add("service", serviceResp.ID)
filter.Add("service", serviceID)
tasks, err := client.TaskList(ctx, types.TaskListOptions{
Filters: filter,
})
@ -207,10 +183,10 @@ func TestCreateServiceSecretFileMode(t *testing.T) {
assert.NilError(t, err)
assert.Check(t, is.Contains(string(content), "-rwxrwxrwx"))
err = client.ServiceRemove(ctx, serviceResp.ID)
err = client.ServiceRemove(ctx, serviceID)
assert.NilError(t, err)
poll.WaitOn(t, serviceIsRemoved(client, serviceResp.ID), swarm.ServicePoll)
poll.WaitOn(t, serviceIsRemoved(client, serviceID), swarm.ServicePoll)
poll.WaitOn(t, noTasks(client), swarm.ServicePoll)
err = client.SecretRemove(ctx, "TestSecret")
@ -234,44 +210,26 @@ func TestCreateServiceConfigFileMode(t *testing.T) {
assert.NilError(t, err)
var instances uint64 = 1
serviceSpec := swarmtypes.ServiceSpec{
Annotations: swarmtypes.Annotations{
Name: "TestService",
},
TaskTemplate: swarmtypes.TaskSpec{
ContainerSpec: &swarmtypes.ContainerSpec{
Image: "busybox:latest",
Command: []string{"/bin/sh", "-c", "ls -l /etc/config || /bin/top"},
Configs: []*swarmtypes.ConfigReference{
{
File: &swarmtypes.ConfigReferenceFileTarget{
Name: "/etc/config",
UID: "0",
GID: "0",
Mode: 0777,
},
ConfigID: configResp.ID,
ConfigName: "TestConfig",
},
},
serviceID := swarm.CreateService(t, d,
swarm.ServiceWithName("TestService"),
swarm.ServiceWithCommand([]string{"/bin/sh", "-c", "ls -l /etc/config || /bin/top"}),
swarm.ServiceWithReplicas(instances),
swarm.ServiceWithConfig(&swarmtypes.ConfigReference{
File: &swarmtypes.ConfigReferenceFileTarget{
Name: "/etc/config",
UID: "0",
GID: "0",
Mode: 0777,
},
},
Mode: swarmtypes.ServiceMode{
Replicated: &swarmtypes.ReplicatedService{
Replicas: &instances,
},
},
}
ConfigID: configResp.ID,
ConfigName: "TestConfig",
}),
)
serviceResp, err := client.ServiceCreate(ctx, serviceSpec, types.ServiceCreateOptions{
QueryRegistry: false,
})
assert.NilError(t, err)
poll.WaitOn(t, serviceRunningTasksCount(client, serviceResp.ID, instances))
poll.WaitOn(t, serviceRunningTasksCount(client, serviceID, instances))
filter := filters.NewArgs()
filter.Add("service", serviceResp.ID)
filter.Add("service", serviceID)
tasks, err := client.TaskList(ctx, types.TaskListOptions{
Filters: filter,
})
@ -288,35 +246,16 @@ func TestCreateServiceConfigFileMode(t *testing.T) {
assert.NilError(t, err)
assert.Check(t, is.Contains(string(content), "-rwxrwxrwx"))
err = client.ServiceRemove(ctx, serviceResp.ID)
err = client.ServiceRemove(ctx, serviceID)
assert.NilError(t, err)
poll.WaitOn(t, serviceIsRemoved(client, serviceResp.ID))
poll.WaitOn(t, serviceIsRemoved(client, serviceID))
poll.WaitOn(t, noTasks(client))
err = client.ConfigRemove(ctx, "TestConfig")
assert.NilError(t, err)
}
func swarmServiceSpec(name string, replicas uint64) swarmtypes.ServiceSpec {
return swarmtypes.ServiceSpec{
Annotations: swarmtypes.Annotations{
Name: name,
},
TaskTemplate: swarmtypes.TaskSpec{
ContainerSpec: &swarmtypes.ContainerSpec{
Image: "busybox:latest",
Command: []string{"/bin/top"},
},
},
Mode: swarmtypes.ServiceMode{
Replicated: &swarmtypes.ReplicatedService{
Replicas: &replicas,
},
},
}
}
func serviceRunningTasksCount(client client.ServiceAPIClient, serviceID string, instances uint64) func(log poll.LogT) poll.Result {
return func(log poll.LogT) poll.Result {
filter := filters.NewArgs()

View File

@ -20,14 +20,14 @@ func TestDockerNetworkConnectAlias(t *testing.T) {
defer client.Close()
ctx := context.Background()
name := "test-alias"
name := t.Name() + "test-alias"
_, err := client.NetworkCreate(ctx, name, types.NetworkCreate{
Driver: "overlay",
Attachable: true,
})
assert.NilError(t, err)
container.Create(t, ctx, client, container.WithName("ng1"), func(c *container.TestContainerConfig) {
cID1 := container.Create(t, ctx, client, func(c *container.TestContainerConfig) {
c.NetworkingConfig = &network.NetworkingConfig{
map[string]*network.EndpointSettings{
name: {},
@ -35,22 +35,22 @@ func TestDockerNetworkConnectAlias(t *testing.T) {
}
})
err = client.NetworkConnect(ctx, name, "ng1", &network.EndpointSettings{
err = client.NetworkConnect(ctx, name, cID1, &network.EndpointSettings{
Aliases: []string{
"aaa",
},
})
assert.NilError(t, err)
err = client.ContainerStart(ctx, "ng1", types.ContainerStartOptions{})
err = client.ContainerStart(ctx, cID1, types.ContainerStartOptions{})
assert.NilError(t, err)
ng1, err := client.ContainerInspect(ctx, "ng1")
ng1, err := client.ContainerInspect(ctx, cID1)
assert.NilError(t, err)
assert.Check(t, is.Equal(len(ng1.NetworkSettings.Networks[name].Aliases), 2))
assert.Check(t, is.Equal(ng1.NetworkSettings.Networks[name].Aliases[0], "aaa"))
container.Create(t, ctx, client, container.WithName("ng2"), func(c *container.TestContainerConfig) {
cID2 := container.Create(t, ctx, client, func(c *container.TestContainerConfig) {
c.NetworkingConfig = &network.NetworkingConfig{
map[string]*network.EndpointSettings{
name: {},
@ -58,17 +58,17 @@ func TestDockerNetworkConnectAlias(t *testing.T) {
}
})
err = client.NetworkConnect(ctx, name, "ng2", &network.EndpointSettings{
err = client.NetworkConnect(ctx, name, cID2, &network.EndpointSettings{
Aliases: []string{
"bbb",
},
})
assert.NilError(t, err)
err = client.ContainerStart(ctx, "ng2", types.ContainerStartOptions{})
err = client.ContainerStart(ctx, cID2, types.ContainerStartOptions{})
assert.NilError(t, err)
ng2, err := client.ContainerInspect(ctx, "ng2")
ng2, err := client.ContainerInspect(ctx, cID2)
assert.NilError(t, err)
assert.Check(t, is.Equal(len(ng2.NetworkSettings.Networks[name].Aliases), 2))
assert.Check(t, is.Equal(ng2.NetworkSettings.Networks[name].Aliases[0], "bbb"))

View File

@ -26,7 +26,7 @@ func TestVolumesCreateAndList(t *testing.T) {
ctx := context.Background()
name := t.Name()
vol, err := client.VolumeCreate(ctx, volumetypes.VolumesCreateBody{
vol, err := client.VolumeCreate(ctx, volumetypes.VolumeCreateBody{
Name: name,
})
assert.NilError(t, err)
@ -83,7 +83,7 @@ func TestVolumesInspect(t *testing.T) {
now := time.Now().Truncate(time.Minute)
name := t.Name()
_, err := client.VolumeCreate(ctx, volumetypes.VolumesCreateBody{
_, err := client.VolumeCreate(ctx, volumetypes.VolumeCreateBody{
Name: name,
})
assert.NilError(t, err)