mirror of
https://github.com/docker/cli.git
synced 2026-11-12 07:43:42 -04:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44e2def671 | ||
|
|
f0e6477914 | ||
|
|
a971796cc5 | ||
|
|
4dd1acceb6 | ||
|
|
85c031e751 | ||
|
|
41aec7703b | ||
|
|
e03fd4a1dd | ||
|
|
252489e04a | ||
|
|
5742bd3ccf | ||
|
|
e3571070d5 | ||
|
|
efb4eb2d0e |
+41
-45
@@ -1,67 +1,63 @@
|
||||
# Changelog
|
||||
|
||||
For more information on the list of deprecated flags and APIs, have a look at
|
||||
https://docs.docker.com/engine/deprecated/ where you can find target removal dates.
|
||||
Items starting with `DEPRECATE` are important deprecation notices. For more
|
||||
information on the list of deprecated flags and APIs please have a look at
|
||||
https://docs.docker.com/engine/deprecated/ where target removal dates can also
|
||||
be found.
|
||||
|
||||
## 18.02.0-ce (2018-02-DD)
|
||||
## 18.01.0-ce (2018-01-DD)
|
||||
|
||||
### Builder
|
||||
|
||||
- Gitutils: fix checking out submodules [moby/moby#35737](https://github.com/moby/moby/pull/35737)
|
||||
* Fix files not being deleted if user-namespaces are enabled [moby/moby#35822](https://github.com/moby/moby/pull/35822)
|
||||
- Add support for expanding environment-variables in `docker commit --change ...` [moby/moby#35582](https://github.com/moby/moby/pull/35582)
|
||||
|
||||
### Client
|
||||
|
||||
* Attach: Ensure attach exit code matches container's [docker/cli#696](https://github.com/docker/cli/pull/696)
|
||||
+ Added support for tmpfs-mode in compose file [docker/cli#808](https://github.com/docker/cli/pull/808)
|
||||
+ Adds a new compose file version 3.6 [docker/cli#808](https://github.com/docker/cli/pull/808)
|
||||
- Fix issue of filter in `docker ps` where `health=starting` returns nothing [moby/moby#35940](https://github.com/moby/moby/pull/35940)
|
||||
+ Improve presentation of published port ranges [docker/cli#581](https://github.com/docker/cli/pull/581)
|
||||
* Return errors from client in stack deploy configs [docker/cli#757](https://github.com/docker/cli/pull/757)
|
||||
- Fix description of filter flag in prune commands [docker/cli#774](https://github.com/docker/cli/pull/774)
|
||||
+ Add "pid" to unsupported options list [docker/cli#768](https://github.com/docker/cli/pull/768)
|
||||
+ Add support for experimental Cli configuration [docker/cli#758](https://github.com/docker/cli/pull/758)
|
||||
+ Add support for generic resources to bash completion [docker/cli#749](https://github.com/docker/cli/pull/749)
|
||||
- Fix error in zsh completion script for docker exec [docker/cli#751](https://github.com/docker/cli/pull/751)
|
||||
+ Add a debug message when client closes websocket attach connection [moby/moby#35720](https://github.com/moby/moby/pull/35720)
|
||||
- Fix bash completion for `"docker swarm"` [docker/cli#772](https://github.com/docker/cli/pull/772)
|
||||
|
||||
|
||||
### Documentation
|
||||
* Correct references to `--publish` long syntax in docs [docker/cli#746](https://github.com/docker/cli/pull/746)
|
||||
* Corrected descriptions for MAC_ADMIN and MAC_OVERRIDE [docker/cli#761](https://github.com/docker/cli/pull/761)
|
||||
* Updated developer doc to explain external CLI [moby/moby#35681](https://github.com/moby/moby/pull/35681)
|
||||
- Fix `"on-failure"` restart policy being documented as "failure" [docker/cli#754](https://github.com/docker/cli/pull/754)
|
||||
- Fix anchors to "Storage driver options" [docker/cli#748](https://github.com/docker/cli/pull/748)
|
||||
|
||||
### Experimental
|
||||
|
||||
+ Add manifest command [docker/cli#138](https://github.com/docker/cli/pull/138)
|
||||
* LCOW remotefs - return error in Read() implementation [moby/moby#36051](https://github.com/moby/moby/pull/36051)
|
||||
+ LCOW: Coalesce daemon stores, allow dual LCOW and WCOW mode [moby/moby#34859](https://github.com/moby/moby/pull/34859)
|
||||
* LCOW: Fix OpenFile parameters [moby/moby#36043](https://github.com/moby/moby/pull/36043)
|
||||
* LCOW: Raise minimum requirement to Windows RS3 RTM build (16299) [moby/moby#36065](https://github.com/moby/moby/pull/36065)
|
||||
+ Add kubernetes support to `docker stack` command [docker/cli#721](https://github.com/docker/cli/pull/721)
|
||||
* Don't append the container id to custom directory checkpoints. [moby/moby#35694](https://github.com/moby/moby/pull/35694)
|
||||
|
||||
### Logging
|
||||
|
||||
* Improve daemon config reload; log active configuration [moby/moby#36019](https://github.com/moby/moby/pull/36019)
|
||||
- Fixed error detection using IsErrNotFound and IsErrNotImplemented for the ContainerLogs method [moby/moby#36000](https://github.com/moby/moby/pull/36000)
|
||||
+ Add journald tag as SYSLOG_IDENTIFIER [moby/moby#35570](https://github.com/moby/moby/pull/35570)
|
||||
* Splunk: limit the reader size on error responses [moby/moby#35509](https://github.com/moby/moby/pull/35509)
|
||||
|
||||
* Fix daemon crash when using the GELF log driver over TCP when the GELF server goes down [moby/moby#35765](https://github.com/moby/moby/pull/35765)
|
||||
- Fix awslogs batch size calculation for large logs [moby/moby#35726](https://github.com/moby/moby/pull/35726)
|
||||
|
||||
### Networking
|
||||
|
||||
* Disable service on release network results in zero-downtime deployments with rolling upgrades [moby/moby#35960](https://github.com/moby/moby/pull/35960)
|
||||
- Fix services failing to start if multiple networks with the same name exist in different spaces [moby/moby#30897](https://github.com/moby/moby/pull/30897)
|
||||
- Fix duplicate networks being added with `docker service update --network-add` [docker/cli#780](https://github.com/docker/cli/pull/780)
|
||||
- Fixing ingress network when upgrading from 17.09 to 17.12. [moby/moby#36003](https://github.com/moby/moby/pull/36003)
|
||||
- Fix ndots configuration [docker/libnetwork#1995](https://github.com/docker/libnetwork/pull/1995)
|
||||
- Fix IPV6 networking being deconfigured if live-restore is enabled [docker/libnetwork#2043](https://github.com/docker/libnetwork/pull/2043)
|
||||
+ Add support for MX type DNS queries in the embedded DNS server [docker/libnetwork#2041](https://github.com/docker/libnetwork/pull/2041)
|
||||
|
||||
### Packaging
|
||||
|
||||
+ Added packaging for Fedora 26, Fedora 27, and Centos 7 on aarch64 [docker/docker-ce-packaging#71](https://github.com/docker/docker-ce-packaging/pull/71)
|
||||
- Removed support for Ubuntu Zesty [docker/docker-ce-packaging#73](https://github.com/docker/docker-ce-packaging/pull/73)
|
||||
- Removed support for Fedora 25 [docker/docker-ce-packaging#72](https://github.com/docker/docker-ce-packaging/pull/72)
|
||||
- Windows: Fix to allow docker service to start on Windows VM [docker/libnetwork#1916](https://github.com/docker/libnetwork/pull/1916)
|
||||
- Fix for docker intercepting DNS requests on ICS network [docker/libnetwork#2014](https://github.com/docker/libnetwork/pull/2014)
|
||||
+ Windows: Added a new network creation driver option [docker/libnetwork#2021](https://github.com/docker/libnetwork/pull/2021)
|
||||
|
||||
|
||||
### Runtime
|
||||
|
||||
* Fixes unexpected Docker Daemon shutdown based on pipe error [moby/moby#35968](https://github.com/moby/moby/pull/35968)
|
||||
* Fix some occurrences of hcsshim::ImportLayer failed in Win32: The system cannot find the path specified [moby/moby#35924](https://github.com/moby/moby/pull/35924)
|
||||
* Windows: increase the maximum layer size during build to 127GB [moby/moby#35925](https://github.com/moby/moby/pull/35925)
|
||||
- Fix Devicemapper: Error running DeleteDevice dm_task_run failed [moby/moby#35919](https://github.com/moby/moby/pull/35919)
|
||||
+ Introduce « exec_die » event [moby/moby#35744](https://github.com/moby/moby/pull/35744)
|
||||
* Update API to version 1.36 [moby/moby#35744](https://github.com/moby/moby/pull/35744)
|
||||
* Fix `docker update` not updating cpu quota, and cpu-period of a running container [moby/moby#36030](https://github.com/moby/moby/pull/36030)
|
||||
* Make container shm parent unbindable [moby/moby#35830](https://github.com/moby/moby/pull/35830)
|
||||
+ Make image (layer) downloads faster by using pigz [moby/moby#35697](https://github.com/moby/moby/pull/35697)
|
||||
+ Protect the daemon from volume plugins that are slow or deadlocked [moby/moby#35441](https://github.com/moby/moby/pull/35441)
|
||||
- Fix `DOCKER_RAMDISK` environment variable not being honoured [moby/moby#35957](https://github.com/moby/moby/pull/35957)
|
||||
* Bump containerd to 1.0.1 (9b55aab90508bd389d7654c4baf173a981477d55) [moby/moby#35986](https://github.com/moby/moby/pull/35986)
|
||||
* Update runc to fix hang during start and exec [moby/moby#36097](https://github.com/moby/moby/pull/36097)
|
||||
* Re-validate Mounts on container start [moby/moby#35833](https://github.com/moby/moby/pull/35833)
|
||||
- Fix overlay2 storage driver inside a user namespace [moby/moby#35794](https://github.com/moby/moby/pull/35794)
|
||||
* Zfs: fix busy error on container stop [moby/moby#35674](https://github.com/moby/moby/pull/35674)
|
||||
- Fix #35843 regression on health check workingdir [moby/moby#35845](https://github.com/moby/moby/pull/35845)
|
||||
- Fix VFS graph driver failure to initialize because of failure to setup fs quota [moby/moby#35827](https://github.com/moby/moby/pull/35827)
|
||||
- Fix containerd events being processed twice [moby/moby#35896](https://github.com/moby/moby/pull/35896)
|
||||
|
||||
### Swarm Mode
|
||||
|
||||
- Fix published ports not being updated if a service has the same number of host-mode published ports with Published Port 0 [docker/swarmkit#2376](https://github.com/docker/swarmkit/pull/2376)
|
||||
* Make the task termination order deterministic [docker/swarmkit#2265](https://github.com/docker/swarmkit/pull/2265)
|
||||
|
||||
@@ -1 +1 @@
|
||||
18.02.0-ce-rc1
|
||||
18.01.0-ce-rc1
|
||||
|
||||
@@ -5,22 +5,16 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/config"
|
||||
cliconfig "github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
cliflags "github.com/docker/cli/cli/flags"
|
||||
manifeststore "github.com/docker/cli/cli/manifest/store"
|
||||
registryclient "github.com/docker/cli/cli/registry/client"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
dopts "github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/go-connections/sockets"
|
||||
"github.com/docker/go-connections/tlsconfig"
|
||||
@@ -51,8 +45,6 @@ type Cli interface {
|
||||
ClientInfo() ClientInfo
|
||||
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
|
||||
DefaultVersion() string
|
||||
ManifestStore() manifeststore.Store
|
||||
RegistryClient(bool) registryclient.RegistryClient
|
||||
}
|
||||
|
||||
// DockerCli is an instance the docker command line client.
|
||||
@@ -122,21 +114,6 @@ func (cli *DockerCli) ClientInfo() ClientInfo {
|
||||
return cli.clientInfo
|
||||
}
|
||||
|
||||
// ManifestStore returns a store for local manifests
|
||||
func (cli *DockerCli) ManifestStore() manifeststore.Store {
|
||||
// TODO: support override default location from config file
|
||||
return manifeststore.NewStore(filepath.Join(config.Dir(), "manifests"))
|
||||
}
|
||||
|
||||
// RegistryClient returns a client for communicating with a Docker distribution
|
||||
// registry
|
||||
func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.RegistryClient {
|
||||
resolver := func(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig {
|
||||
return ResolveAuthConfig(ctx, cli, index)
|
||||
}
|
||||
return registryclient.NewRegistryClient(resolver, UserAgent(), allowInsecure)
|
||||
}
|
||||
|
||||
// Initialize the dockerCli runs initialization that must happen after command
|
||||
// line flags are parsed.
|
||||
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/docker/cli/cli/command/config"
|
||||
"github.com/docker/cli/cli/command/container"
|
||||
"github.com/docker/cli/cli/command/image"
|
||||
"github.com/docker/cli/cli/command/manifest"
|
||||
"github.com/docker/cli/cli/command/network"
|
||||
"github.com/docker/cli/cli/command/node"
|
||||
"github.com/docker/cli/cli/command/plugin"
|
||||
@@ -40,15 +39,12 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) {
|
||||
image.NewImageCommand(dockerCli),
|
||||
image.NewBuildCommand(dockerCli),
|
||||
|
||||
// manifest
|
||||
manifest.NewManifestCommand(dockerCli),
|
||||
// node
|
||||
node.NewNodeCommand(dockerCli),
|
||||
|
||||
// network
|
||||
network.NewNetworkCommand(dockerCli),
|
||||
|
||||
// node
|
||||
node.NewNodeCommand(dockerCli),
|
||||
|
||||
// plugin
|
||||
plugin.NewPluginCommand(dockerCli),
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ func newConfigCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "create [OPTIONS] CONFIG file|-",
|
||||
Short: "Create a config from a file or STDIN",
|
||||
Short: "Create a configuration file from a file or STDIN as content",
|
||||
Args: cli.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
createOpts.name = args[0]
|
||||
|
||||
@@ -21,7 +21,7 @@ func newConfigInspectCommand(dockerCli command.Cli) *cobra.Command {
|
||||
opts := inspectOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "inspect [OPTIONS] CONFIG [CONFIG...]",
|
||||
Short: "Display detailed information on one or more configs",
|
||||
Short: "Display detailed information on one or more configuration files",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.names = args
|
||||
|
||||
@@ -19,7 +19,7 @@ func newConfigRemoveCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "rm CONFIG [CONFIG...]",
|
||||
Aliases: []string{"remove"},
|
||||
Short: "Remove one or more configs",
|
||||
Short: "Remove one or more configuration files",
|
||||
Args: cli.RequiresMinArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts := removeOptions{
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http/httputil"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/signal"
|
||||
"github.com/pkg/errors"
|
||||
@@ -68,9 +66,6 @@ func runAttach(dockerCli command.Cli, opts *attachOptions) error {
|
||||
ctx := context.Background()
|
||||
client := dockerCli.Client()
|
||||
|
||||
// request channel to wait for client
|
||||
resultC, errC := client.ContainerWait(ctx, opts.container, "")
|
||||
|
||||
c, err := inspectContainerAndCheckState(ctx, client, opts.container)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -145,24 +140,7 @@ func runAttach(dockerCli command.Cli, opts *attachOptions) error {
|
||||
if errAttach != nil {
|
||||
return errAttach
|
||||
}
|
||||
|
||||
return getExitStatus(errC, resultC)
|
||||
}
|
||||
|
||||
func getExitStatus(errC <-chan error, resultC <-chan container.ContainerWaitOKBody) error {
|
||||
select {
|
||||
case result := <-resultC:
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf(result.Error.Message)
|
||||
}
|
||||
if result.StatusCode != 0 {
|
||||
return cli.StatusError{StatusCode: int(result.StatusCode)}
|
||||
}
|
||||
case err := <-errC:
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return getExitStatus(ctx, dockerCli.Client(), opts.container)
|
||||
}
|
||||
|
||||
func resizeTTY(ctx context.Context, dockerCli command.Cli, containerID string) {
|
||||
@@ -179,3 +157,19 @@ func resizeTTY(ctx context.Context, dockerCli command.Cli, containerID string) {
|
||||
logrus.Debugf("Error monitoring TTY size: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func getExitStatus(ctx context.Context, apiclient client.ContainerAPIClient, containerID string) error {
|
||||
container, err := apiclient.ContainerInspect(ctx, containerID)
|
||||
if err != nil {
|
||||
// If we can't connect, then the daemon probably died.
|
||||
if !client.IsErrConnectionFailed(err) {
|
||||
return err
|
||||
}
|
||||
return cli.StatusError{StatusCode: -1}
|
||||
}
|
||||
status := container.State.ExitCode
|
||||
if status != 0 {
|
||||
return cli.StatusError{StatusCode: status}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
@@ -9,9 +8,9 @@ import (
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/testutil"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestNewAttachCommandErrors(t *testing.T) {
|
||||
@@ -79,50 +78,40 @@ func TestNewAttachCommandErrors(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetExitStatus(t *testing.T) {
|
||||
var (
|
||||
expectedErr = fmt.Errorf("unexpected error")
|
||||
errC = make(chan error, 1)
|
||||
resultC = make(chan container.ContainerWaitOKBody, 1)
|
||||
)
|
||||
containerID := "the exec id"
|
||||
expecatedErr := errors.New("unexpected error")
|
||||
|
||||
testcases := []struct {
|
||||
result *container.ContainerWaitOKBody
|
||||
err error
|
||||
inspectError error
|
||||
exitCode int
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
result: &container.ContainerWaitOKBody{
|
||||
StatusCode: 0,
|
||||
},
|
||||
inspectError: nil,
|
||||
exitCode: 0,
|
||||
},
|
||||
{
|
||||
err: expectedErr,
|
||||
expectedError: expectedErr,
|
||||
inspectError: expecatedErr,
|
||||
expectedError: expecatedErr,
|
||||
},
|
||||
{
|
||||
result: &container.ContainerWaitOKBody{
|
||||
Error: &container.ContainerWaitOKBodyError{
|
||||
expectedErr.Error(),
|
||||
},
|
||||
},
|
||||
expectedError: expectedErr,
|
||||
},
|
||||
{
|
||||
result: &container.ContainerWaitOKBody{
|
||||
StatusCode: 15,
|
||||
},
|
||||
exitCode: 15,
|
||||
expectedError: cli.StatusError{StatusCode: 15},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
if testcase.err != nil {
|
||||
errC <- testcase.err
|
||||
client := &fakeClient{
|
||||
inspectFunc: func(id string) (types.ContainerJSON, error) {
|
||||
assert.Equal(t, containerID, id)
|
||||
return types.ContainerJSON{
|
||||
ContainerJSONBase: &types.ContainerJSONBase{
|
||||
State: &types.ContainerState{ExitCode: testcase.exitCode},
|
||||
},
|
||||
}, testcase.inspectError
|
||||
},
|
||||
}
|
||||
if testcase.result != nil {
|
||||
resultC <- *testcase.result
|
||||
}
|
||||
err := getExitStatus(errC, resultC)
|
||||
err := getExitStatus(context.Background(), client, containerID)
|
||||
assert.Equal(t, testcase.expectedError, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,6 @@ type fakeClient struct {
|
||||
containerStatPathFunc func(container, path string) (types.ContainerPathStat, error)
|
||||
containerCopyFromFunc func(container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error)
|
||||
logFunc func(string, types.ContainerLogsOptions) (io.ReadCloser, error)
|
||||
waitFunc func(string) (<-chan container.ContainerWaitOKBody, <-chan error)
|
||||
Version string
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (types.ContainerJSON, error) {
|
||||
@@ -97,14 +95,3 @@ func (f *fakeClient) ContainerLogs(_ context.Context, container string, options
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ClientVersion() string {
|
||||
return f.Version
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerWait(_ context.Context, container string, _ container.WaitCondition) (<-chan container.ContainerWaitOKBody, <-chan error) {
|
||||
if f.waitFunc != nil {
|
||||
return f.waitFunc(container)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -37,12 +37,7 @@ func waitExitOrRemoved(ctx context.Context, dockerCli command.Cli, containerID s
|
||||
go func() {
|
||||
select {
|
||||
case result := <-resultC:
|
||||
if result.Error != nil {
|
||||
logrus.Errorf("Error waiting for container: %v", result.Error.Message)
|
||||
statusC <- 125
|
||||
} else {
|
||||
statusC <- int(result.StatusCode)
|
||||
}
|
||||
statusC <- int(result.StatusCode)
|
||||
case err := <-errC:
|
||||
logrus.Errorf("error waiting for container: %v", err)
|
||||
statusC <- 125
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func waitFn(cid string) (<-chan container.ContainerWaitOKBody, <-chan error) {
|
||||
resC := make(chan container.ContainerWaitOKBody)
|
||||
errC := make(chan error, 1)
|
||||
var res container.ContainerWaitOKBody
|
||||
|
||||
go func() {
|
||||
switch {
|
||||
case strings.Contains(cid, "exit-code-42"):
|
||||
res.StatusCode = 42
|
||||
resC <- res
|
||||
case strings.Contains(cid, "non-existent"):
|
||||
err := errors.Errorf("No such container: %v", cid)
|
||||
errC <- err
|
||||
case strings.Contains(cid, "wait-error"):
|
||||
res.Error = &container.ContainerWaitOKBodyError{Message: "removal failed"}
|
||||
resC <- res
|
||||
default:
|
||||
// normal exit
|
||||
resC <- res
|
||||
}
|
||||
}()
|
||||
|
||||
return resC, errC
|
||||
}
|
||||
|
||||
func TestWaitExitOrRemoved(t *testing.T) {
|
||||
testcases := []struct {
|
||||
cid string
|
||||
exitCode int
|
||||
}{
|
||||
{
|
||||
cid: "normal-container",
|
||||
exitCode: 0,
|
||||
},
|
||||
{
|
||||
cid: "give-me-exit-code-42",
|
||||
exitCode: 42,
|
||||
},
|
||||
{
|
||||
cid: "i-want-a-wait-error",
|
||||
exitCode: 125,
|
||||
},
|
||||
{
|
||||
cid: "non-existent-container-id",
|
||||
exitCode: 125,
|
||||
},
|
||||
}
|
||||
|
||||
client := test.NewFakeCli(&fakeClient{waitFunc: waitFn, Version: api.DefaultVersion})
|
||||
for _, testcase := range testcases {
|
||||
statusC := waitExitOrRemoved(context.Background(), client, testcase.cid, true)
|
||||
exitCode := <-statusC
|
||||
assert.Equal(t, testcase.exitCode, exitCode)
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package formatter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -521,95 +520,19 @@ func (c *serviceContext) Image() string {
|
||||
return image
|
||||
}
|
||||
|
||||
type portRange struct {
|
||||
pStart uint32
|
||||
pEnd uint32
|
||||
tStart uint32
|
||||
tEnd uint32
|
||||
protocol swarm.PortConfigProtocol
|
||||
}
|
||||
|
||||
func (pr portRange) String() string {
|
||||
var (
|
||||
pub string
|
||||
tgt string
|
||||
)
|
||||
|
||||
if pr.pEnd > pr.pStart {
|
||||
pub = fmt.Sprintf("%d-%d", pr.pStart, pr.pEnd)
|
||||
} else {
|
||||
pub = fmt.Sprintf("%d", pr.pStart)
|
||||
}
|
||||
if pr.tEnd > pr.tStart {
|
||||
tgt = fmt.Sprintf("%d-%d", pr.tStart, pr.tEnd)
|
||||
} else {
|
||||
tgt = fmt.Sprintf("%d", pr.tStart)
|
||||
}
|
||||
return fmt.Sprintf("*:%s->%s/%s", pub, tgt, pr.protocol)
|
||||
}
|
||||
|
||||
// Ports formats published ports on the ingress network for output.
|
||||
//
|
||||
// Where possible, ranges are grouped to produce a compact output:
|
||||
// - multiple ports mapped to a single port (80->80, 81->80); is formatted as *:80-81->80
|
||||
// - multiple consecutive ports on both sides; (80->80, 81->81) are formatted as: *:80-81->80-81
|
||||
//
|
||||
// The above should not be grouped together, i.e.:
|
||||
// - 80->80, 81->81, 82->80 should be presented as : *:80-81->80-81, *:82->80
|
||||
//
|
||||
// TODO improve:
|
||||
// - combine non-consecutive ports mapped to a single port (80->80, 81->80, 84->80, 86->80, 87->80); to be printed as *:80-81,84,86-87->80
|
||||
// - combine tcp and udp mappings if their port-mapping is exactly the same (*:80-81->80-81/tcp+udp instead of *:80-81->80-81/tcp, *:80-81->80-81/udp)
|
||||
func (c *serviceContext) Ports() string {
|
||||
if c.service.Endpoint.Ports == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
pr := portRange{}
|
||||
ports := []string{}
|
||||
|
||||
sort.Sort(byProtocolAndPublishedPort(c.service.Endpoint.Ports))
|
||||
|
||||
for _, p := range c.service.Endpoint.Ports {
|
||||
if p.PublishMode == swarm.PortConfigPublishModeIngress {
|
||||
prIsRange := pr.tEnd != pr.tStart
|
||||
tOverlaps := p.TargetPort <= pr.tEnd
|
||||
|
||||
// Start a new port-range if:
|
||||
// - the protocol is different from the current port-range
|
||||
// - published or target port are not consecutive to the current port-range
|
||||
// - the current port-range is a _range_, and the target port overlaps with the current range's target-ports
|
||||
if p.Protocol != pr.protocol || p.PublishedPort-pr.pEnd > 1 || p.TargetPort-pr.tEnd > 1 || prIsRange && tOverlaps {
|
||||
// start a new port-range, and print the previous port-range (if any)
|
||||
if pr.pStart > 0 {
|
||||
ports = append(ports, pr.String())
|
||||
}
|
||||
pr = portRange{
|
||||
pStart: p.PublishedPort,
|
||||
pEnd: p.PublishedPort,
|
||||
tStart: p.TargetPort,
|
||||
tEnd: p.TargetPort,
|
||||
protocol: p.Protocol,
|
||||
}
|
||||
continue
|
||||
}
|
||||
pr.pEnd = p.PublishedPort
|
||||
pr.tEnd = p.TargetPort
|
||||
for _, pConfig := range c.service.Endpoint.Ports {
|
||||
if pConfig.PublishMode == swarm.PortConfigPublishModeIngress {
|
||||
ports = append(ports, fmt.Sprintf("*:%d->%d/%s",
|
||||
pConfig.PublishedPort,
|
||||
pConfig.TargetPort,
|
||||
pConfig.Protocol,
|
||||
))
|
||||
}
|
||||
}
|
||||
if pr.pStart > 0 {
|
||||
ports = append(ports, pr.String())
|
||||
}
|
||||
return strings.Join(ports, ", ")
|
||||
}
|
||||
|
||||
type byProtocolAndPublishedPort []swarm.PortConfig
|
||||
|
||||
func (a byProtocolAndPublishedPort) Len() int { return len(a) }
|
||||
func (a byProtocolAndPublishedPort) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a byProtocolAndPublishedPort) Less(i, j int) bool {
|
||||
if a[i].Protocol == a[j].Protocol {
|
||||
return a[i].PublishedPort < a[j].PublishedPort
|
||||
}
|
||||
return a[i].Protocol < a[j].Protocol
|
||||
return strings.Join(ports, ",")
|
||||
}
|
||||
|
||||
@@ -224,124 +224,3 @@ func TestServiceContextWriteJSONField(t *testing.T) {
|
||||
assert.Equal(t, services[i].Spec.Name, s, msg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceContext_Ports(t *testing.T) {
|
||||
c := serviceContext{
|
||||
service: swarm.Service{
|
||||
Endpoint: swarm.Endpoint{
|
||||
Ports: []swarm.PortConfig{
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 80,
|
||||
PublishedPort: 81,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 80,
|
||||
PublishedPort: 80,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 95,
|
||||
PublishedPort: 95,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 90,
|
||||
PublishedPort: 90,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 91,
|
||||
PublishedPort: 91,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 92,
|
||||
PublishedPort: 92,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 93,
|
||||
PublishedPort: 93,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 94,
|
||||
PublishedPort: 94,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "udp",
|
||||
TargetPort: 95,
|
||||
PublishedPort: 95,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "udp",
|
||||
TargetPort: 90,
|
||||
PublishedPort: 90,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "udp",
|
||||
TargetPort: 96,
|
||||
PublishedPort: 96,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "udp",
|
||||
TargetPort: 91,
|
||||
PublishedPort: 91,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "udp",
|
||||
TargetPort: 92,
|
||||
PublishedPort: 92,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "udp",
|
||||
TargetPort: 93,
|
||||
PublishedPort: 93,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "udp",
|
||||
TargetPort: 94,
|
||||
PublishedPort: 94,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 60,
|
||||
PublishedPort: 60,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 61,
|
||||
PublishedPort: 61,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 61,
|
||||
PublishedPort: 62,
|
||||
PublishMode: "ingress",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, "*:60-61->60-61/tcp, *:62->61/tcp, *:80-81->80/tcp, *:90-95->90-95/tcp, *:90-96->90-96/udp", c.Ports())
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ func runRemove(dockerCli command.Cli, opts removeOptions, images []string) error
|
||||
if !opts.force || fatalErr {
|
||||
return errors.New(msg)
|
||||
}
|
||||
fmt.Fprintln(dockerCli.Err(), msg)
|
||||
fmt.Fprintf(dockerCli.Err(), msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
|
||||
assert.Equal(t, true, options.Force)
|
||||
return []types.ImageDeleteResponseItem{}, notFound{"image1"}
|
||||
},
|
||||
expectedStderr: "Error: No such image: image1\n",
|
||||
expectedStderr: "Error: No such image: image1",
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/manifest/store"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type annotateOptions struct {
|
||||
target string // the target manifest list name (also transaction ID)
|
||||
image string // the manifest to annotate within the list
|
||||
variant string // an architecture variant
|
||||
os string
|
||||
arch string
|
||||
osFeatures []string
|
||||
}
|
||||
|
||||
// NewAnnotateCommand creates a new `docker manifest annotate` command
|
||||
func newAnnotateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts annotateOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "annotate [OPTIONS] MANIFEST_LIST MANIFEST",
|
||||
Short: "Add additional information to a local image manifest",
|
||||
Args: cli.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.target = args[0]
|
||||
opts.image = args[1]
|
||||
return runManifestAnnotate(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
|
||||
flags.StringVar(&opts.os, "os", "", "Set operating system")
|
||||
flags.StringVar(&opts.arch, "arch", "", "Set architecture")
|
||||
flags.StringSliceVar(&opts.osFeatures, "os-features", []string{}, "Set operating system feature")
|
||||
flags.StringVar(&opts.variant, "variant", "", "Set architecture variant")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runManifestAnnotate(dockerCli command.Cli, opts annotateOptions) error {
|
||||
targetRef, err := normalizeReference(opts.target)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "annotate: error parsing name for manifest list %s", opts.target)
|
||||
}
|
||||
imgRef, err := normalizeReference(opts.image)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "annotate: error parsing name for manifest %s", opts.image)
|
||||
}
|
||||
|
||||
manifestStore := dockerCli.ManifestStore()
|
||||
imageManifest, err := manifestStore.Get(targetRef, imgRef)
|
||||
switch {
|
||||
case store.IsNotFound(err):
|
||||
return fmt.Errorf("manifest for image %s does not exist in %s", opts.image, opts.target)
|
||||
case err != nil:
|
||||
return err
|
||||
}
|
||||
|
||||
// Update the mf
|
||||
if opts.os != "" {
|
||||
imageManifest.Platform.OS = opts.os
|
||||
}
|
||||
if opts.arch != "" {
|
||||
imageManifest.Platform.Architecture = opts.arch
|
||||
}
|
||||
for _, osFeature := range opts.osFeatures {
|
||||
imageManifest.Platform.OSFeatures = appendIfUnique(imageManifest.Platform.OSFeatures, osFeature)
|
||||
}
|
||||
if opts.variant != "" {
|
||||
imageManifest.Platform.Variant = opts.variant
|
||||
}
|
||||
|
||||
if !isValidOSArch(imageManifest.Platform.OS, imageManifest.Platform.Architecture) {
|
||||
return errors.Errorf("manifest entry for image has unsupported os/arch combination: %s/%s", opts.os, opts.arch)
|
||||
}
|
||||
return manifestStore.Save(targetRef, imgRef, imageManifest)
|
||||
}
|
||||
|
||||
func appendIfUnique(list []string, str string) []string {
|
||||
for _, s := range list {
|
||||
if s == str {
|
||||
return list
|
||||
}
|
||||
}
|
||||
return append(list, str)
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/testutil"
|
||||
"github.com/gotestyourself/gotestyourself/golden"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestManifestAnnotateError(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
args: []string{"too-few-arguments"},
|
||||
expectedError: "requires exactly 2 arguments",
|
||||
},
|
||||
{
|
||||
args: []string{"th!si'sa/fa!ke/li$t/name", "example.com/alpine:3.0"},
|
||||
expectedError: "error parsing name for manifest list",
|
||||
},
|
||||
{
|
||||
args: []string{"example.com/list:v1", "th!si'sa/fa!ke/im@ge/nam32"},
|
||||
expectedError: "error parsing name for manifest",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(nil)
|
||||
cmd := newAnnotateCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManifestAnnotate(t *testing.T) {
|
||||
store, cleanup := newTempManifestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
cli := test.NewFakeCli(nil)
|
||||
cli.SetManifestStore(store)
|
||||
namedRef := ref(t, "alpine:3.0")
|
||||
imageManifest := fullImageManifest(t, namedRef)
|
||||
err := store.Save(ref(t, "list:v1"), namedRef, imageManifest)
|
||||
require.NoError(t, err)
|
||||
|
||||
cmd := newAnnotateCommand(cli)
|
||||
cmd.SetArgs([]string{"example.com/list:v1", "example.com/fake:0.0"})
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
expectedError := "manifest for image example.com/fake:0.0 does not exist"
|
||||
testutil.ErrorContains(t, cmd.Execute(), expectedError)
|
||||
|
||||
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"})
|
||||
cmd.Flags().Set("os", "freebsd")
|
||||
cmd.Flags().Set("arch", "fake")
|
||||
cmd.Flags().Set("os-features", "feature1")
|
||||
cmd.Flags().Set("variant", "v7")
|
||||
expectedError = "manifest entry for image has unsupported os/arch combination"
|
||||
testutil.ErrorContains(t, cmd.Execute(), expectedError)
|
||||
|
||||
cmd.Flags().Set("arch", "arm")
|
||||
require.NoError(t, cmd.Execute())
|
||||
|
||||
cmd = newInspectCommand(cli)
|
||||
err = cmd.Flags().Set("verbose", "true")
|
||||
require.NoError(t, err)
|
||||
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"})
|
||||
require.NoError(t, cmd.Execute())
|
||||
actual := cli.OutBuffer()
|
||||
expected := golden.Get(t, "inspect-annotate.golden")
|
||||
assert.Equal(t, string(expected), actual.String())
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
manifesttypes "github.com/docker/cli/cli/manifest/types"
|
||||
"github.com/docker/cli/cli/registry/client"
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type fakeRegistryClient struct {
|
||||
client.RegistryClient
|
||||
getManifestFunc func(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error)
|
||||
getManifestListFunc func(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error)
|
||||
mountBlobFunc func(ctx context.Context, source reference.Canonical, target reference.Named) error
|
||||
putManifestFunc func(ctx context.Context, source reference.Named, mf distribution.Manifest) (digest.Digest, error)
|
||||
}
|
||||
|
||||
func (c *fakeRegistryClient) GetManifest(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) {
|
||||
if c.getManifestFunc != nil {
|
||||
return c.getManifestFunc(ctx, ref)
|
||||
}
|
||||
return manifesttypes.ImageManifest{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeRegistryClient) GetManifestList(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error) {
|
||||
if c.getManifestListFunc != nil {
|
||||
return c.getManifestListFunc(ctx, ref)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *fakeRegistryClient) MountBlob(ctx context.Context, source reference.Canonical, target reference.Named) error {
|
||||
if c.mountBlobFunc != nil {
|
||||
return c.mountBlobFunc(ctx, source, target)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *fakeRegistryClient) PutManifest(ctx context.Context, ref reference.Named, mf distribution.Manifest) (digest.Digest, error) {
|
||||
if c.putManifestFunc != nil {
|
||||
return c.putManifestFunc(ctx, ref, mf)
|
||||
}
|
||||
return digest.Digest(""), nil
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NewManifestCommand returns a cobra command for `manifest` subcommands
|
||||
func NewManifestCommand(dockerCli command.Cli) *cobra.Command {
|
||||
// use dockerCli as command.Cli
|
||||
cmd := &cobra.Command{
|
||||
Use: "manifest COMMAND",
|
||||
Short: "Manage Docker image manifests and manifest lists",
|
||||
Long: manifestDescription,
|
||||
Args: cli.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
|
||||
},
|
||||
Annotations: map[string]string{"experimentalCLI": ""},
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newCreateListCommand(dockerCli),
|
||||
newInspectCommand(dockerCli),
|
||||
newAnnotateCommand(dockerCli),
|
||||
newPushListCommand(dockerCli),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
var manifestDescription = `
|
||||
The **docker manifest** command has subcommands for managing image manifests and
|
||||
manifest lists. A manifest list allows you to use one name to refer to the same image
|
||||
built for multiple architectures.
|
||||
|
||||
To see help for a subcommand, use:
|
||||
|
||||
docker manifest CMD --help
|
||||
|
||||
For full details on using docker manifest lists, see the registry v2 specification.
|
||||
|
||||
`
|
||||
@@ -1,82 +0,0 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/manifest/store"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type createOpts struct {
|
||||
amend bool
|
||||
insecure bool
|
||||
}
|
||||
|
||||
func newCreateListCommand(dockerCli command.Cli) *cobra.Command {
|
||||
opts := createOpts{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "create MANFEST_LIST MANIFEST [MANIFEST...]",
|
||||
Short: "Create a local manifest list for annotating and pushing to a registry",
|
||||
Args: cli.RequiresMinArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return createManifestList(dockerCli, args, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVar(&opts.insecure, "insecure", false, "allow communication with an insecure registry")
|
||||
flags.BoolVarP(&opts.amend, "amend", "a", false, "Amend an existing manifest list")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func createManifestList(dockerCli command.Cli, args []string, opts createOpts) error {
|
||||
newRef := args[0]
|
||||
targetRef, err := normalizeReference(newRef)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error parsing name for manifest list %s", newRef)
|
||||
}
|
||||
|
||||
_, err = registry.ParseRepositoryInfo(targetRef)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error parsing repository name for manifest list %s", newRef)
|
||||
}
|
||||
|
||||
manifestStore := dockerCli.ManifestStore()
|
||||
_, err = manifestStore.GetList(targetRef)
|
||||
switch {
|
||||
case store.IsNotFound(err):
|
||||
// New manifest list
|
||||
case err != nil:
|
||||
return err
|
||||
case !opts.amend:
|
||||
return errors.Errorf("refusing to amend an existing manifest list with no --amend flag")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
// Now create the local manifest list transaction by looking up the manifest schemas
|
||||
// for the constituent images:
|
||||
manifests := args[1:]
|
||||
for _, manifestRef := range manifests {
|
||||
namedRef, err := normalizeReference(manifestRef)
|
||||
if err != nil {
|
||||
// TODO: wrap error?
|
||||
return err
|
||||
}
|
||||
|
||||
manifest, err := getManifest(ctx, dockerCli, targetRef, namedRef, opts.insecure)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := manifestStore.Save(targetRef, namedRef, manifest); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(dockerCli.Out(), "Created manifest list %s\n", targetRef.String())
|
||||
return nil
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
manifesttypes "github.com/docker/cli/cli/manifest/types"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/testutil"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/gotestyourself/gotestyourself/golden"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestManifestCreateErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
args: []string{"too-few-arguments"},
|
||||
expectedError: "requires at least 2 arguments",
|
||||
},
|
||||
{
|
||||
args: []string{"th!si'sa/fa!ke/li$t/name", "example.com/alpine:3.0"},
|
||||
expectedError: "error parsing name for manifest list",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(nil)
|
||||
cmd := newCreateListCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
// create a manifest list, then overwrite it, and inspect to see if the old one is still there
|
||||
func TestManifestCreateAmend(t *testing.T) {
|
||||
store, cleanup := newTempManifestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
cli := test.NewFakeCli(nil)
|
||||
cli.SetManifestStore(store)
|
||||
|
||||
namedRef := ref(t, "alpine:3.0")
|
||||
imageManifest := fullImageManifest(t, namedRef)
|
||||
err := store.Save(ref(t, "list:v1"), namedRef, imageManifest)
|
||||
require.NoError(t, err)
|
||||
namedRef = ref(t, "alpine:3.1")
|
||||
imageManifest = fullImageManifest(t, namedRef)
|
||||
err = store.Save(ref(t, "list:v1"), namedRef, imageManifest)
|
||||
require.NoError(t, err)
|
||||
|
||||
cmd := newCreateListCommand(cli)
|
||||
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.1"})
|
||||
cmd.Flags().Set("amend", "true")
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
err = cmd.Execute()
|
||||
require.NoError(t, err)
|
||||
|
||||
// make a new cli to clear the buffers
|
||||
cli = test.NewFakeCli(nil)
|
||||
cli.SetManifestStore(store)
|
||||
inspectCmd := newInspectCommand(cli)
|
||||
inspectCmd.SetArgs([]string{"example.com/list:v1"})
|
||||
require.NoError(t, inspectCmd.Execute())
|
||||
actual := cli.OutBuffer()
|
||||
expected := golden.Get(t, "inspect-manifest-list.golden")
|
||||
assert.Equal(t, string(expected), actual.String())
|
||||
}
|
||||
|
||||
// attempt to overwrite a saved manifest and get refused
|
||||
func TestManifestCreateRefuseAmend(t *testing.T) {
|
||||
store, cleanup := newTempManifestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
cli := test.NewFakeCli(nil)
|
||||
cli.SetManifestStore(store)
|
||||
namedRef := ref(t, "alpine:3.0")
|
||||
imageManifest := fullImageManifest(t, namedRef)
|
||||
err := store.Save(ref(t, "list:v1"), namedRef, imageManifest)
|
||||
require.NoError(t, err)
|
||||
|
||||
cmd := newCreateListCommand(cli)
|
||||
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"})
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
err = cmd.Execute()
|
||||
assert.EqualError(t, err, "refusing to amend an existing manifest list with no --amend flag")
|
||||
}
|
||||
|
||||
// attempt to make a manifest list without valid images
|
||||
func TestManifestCreateNoManifest(t *testing.T) {
|
||||
store, cleanup := newTempManifestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
cli := test.NewFakeCli(nil)
|
||||
cli.SetManifestStore(store)
|
||||
cli.SetRegistryClient(&fakeRegistryClient{
|
||||
getManifestFunc: func(_ context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) {
|
||||
return manifesttypes.ImageManifest{}, errors.Errorf("No such image: %v", ref)
|
||||
},
|
||||
getManifestListFunc: func(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error) {
|
||||
return nil, errors.Errorf("No such manifest: %s", ref)
|
||||
},
|
||||
})
|
||||
|
||||
cmd := newCreateListCommand(cli)
|
||||
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"})
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
err := cmd.Execute()
|
||||
assert.EqualError(t, err, "No such image: example.com/alpine:3.0")
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/manifest/types"
|
||||
"github.com/docker/distribution/manifest/manifestlist"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type inspectOptions struct {
|
||||
ref string
|
||||
list string
|
||||
verbose bool
|
||||
insecure bool
|
||||
}
|
||||
|
||||
// NewInspectCommand creates a new `docker manifest inspect` command
|
||||
func newInspectCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts inspectOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "inspect [OPTIONS] [MANIFEST_LIST] MANIFEST",
|
||||
Short: "Display an image manifest, or manifest list",
|
||||
Args: cli.RequiresRangeArgs(1, 2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
switch len(args) {
|
||||
case 1:
|
||||
opts.ref = args[0]
|
||||
case 2:
|
||||
opts.list = args[0]
|
||||
opts.ref = args[1]
|
||||
}
|
||||
return runInspect(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVar(&opts.insecure, "insecure", false, "allow communication with an insecure registry")
|
||||
flags.BoolVarP(&opts.verbose, "verbose", "v", false, "Output additional info including layers and platform")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runInspect(dockerCli command.Cli, opts inspectOptions) error {
|
||||
namedRef, err := normalizeReference(opts.ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If list reference is provided, display the local manifest in a list
|
||||
if opts.list != "" {
|
||||
listRef, err := normalizeReference(opts.list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imageManifest, err := dockerCli.ManifestStore().Get(listRef, namedRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printManifest(dockerCli, imageManifest, opts)
|
||||
}
|
||||
|
||||
// Try a local manifest list first
|
||||
localManifestList, err := dockerCli.ManifestStore().GetList(namedRef)
|
||||
if err == nil {
|
||||
return printManifestList(dockerCli, namedRef, localManifestList, opts)
|
||||
}
|
||||
|
||||
// Next try a remote manifest
|
||||
ctx := context.Background()
|
||||
registryClient := dockerCli.RegistryClient(opts.insecure)
|
||||
imageManifest, err := registryClient.GetManifest(ctx, namedRef)
|
||||
if err == nil {
|
||||
return printManifest(dockerCli, imageManifest, opts)
|
||||
}
|
||||
|
||||
// Finally try a remote manifest list
|
||||
manifestList, err := registryClient.GetManifestList(ctx, namedRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printManifestList(dockerCli, namedRef, manifestList, opts)
|
||||
}
|
||||
|
||||
func printManifest(dockerCli command.Cli, manifest types.ImageManifest, opts inspectOptions) error {
|
||||
buffer := new(bytes.Buffer)
|
||||
if !opts.verbose {
|
||||
_, raw, err := manifest.Payload()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Indent(buffer, raw, "", "\t"); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(dockerCli.Out(), buffer.String())
|
||||
return nil
|
||||
}
|
||||
jsonBytes, err := json.MarshalIndent(manifest, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dockerCli.Out().Write(append(jsonBytes, '\n'))
|
||||
return nil
|
||||
}
|
||||
|
||||
func printManifestList(dockerCli command.Cli, namedRef reference.Named, list []types.ImageManifest, opts inspectOptions) error {
|
||||
if !opts.verbose {
|
||||
targetRepo, err := registry.ParseRepositoryInfo(namedRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manifests := []manifestlist.ManifestDescriptor{}
|
||||
// More than one response. This is a manifest list.
|
||||
for _, img := range list {
|
||||
mfd, err := buildManifestDescriptor(targetRepo, img)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error assembling ManifestDescriptor")
|
||||
}
|
||||
manifests = append(manifests, mfd)
|
||||
}
|
||||
deserializedML, err := manifestlist.FromDescriptors(manifests)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
jsonBytes, err := deserializedML.MarshalJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(dockerCli.Out(), string(jsonBytes))
|
||||
return nil
|
||||
}
|
||||
jsonBytes, err := json.MarshalIndent(list, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dockerCli.Out().Write(append(jsonBytes, '\n'))
|
||||
return nil
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/manifest/store"
|
||||
"github.com/docker/cli/cli/manifest/types"
|
||||
manifesttypes "github.com/docker/cli/cli/manifest/types"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/gotestyourself/gotestyourself/golden"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func newTempManifestStore(t *testing.T) (store.Store, func()) {
|
||||
tmpdir, err := ioutil.TempDir("", "test-manifest-storage")
|
||||
require.NoError(t, err)
|
||||
|
||||
return store.NewStore(tmpdir), func() { os.RemoveAll(tmpdir) }
|
||||
}
|
||||
|
||||
func ref(t *testing.T, name string) reference.Named {
|
||||
named, err := reference.ParseNamed("example.com/" + name)
|
||||
require.NoError(t, err)
|
||||
return named
|
||||
}
|
||||
|
||||
func fullImageManifest(t *testing.T, ref reference.Named) types.ImageManifest {
|
||||
man, err := schema2.FromStruct(schema2.Manifest{
|
||||
Versioned: schema2.SchemaVersion,
|
||||
Config: distribution.Descriptor{
|
||||
Digest: "sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d624560",
|
||||
Size: 1520,
|
||||
MediaType: schema2.MediaTypeImageConfig,
|
||||
},
|
||||
Layers: []distribution.Descriptor{
|
||||
{
|
||||
MediaType: schema2.MediaTypeLayer,
|
||||
Size: 1990402,
|
||||
Digest: "sha256:88286f41530e93dffd4b964e1db22ce4939fffa4a4c665dab8591fbab03d4926",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// TODO: include image data for verbose inspect
|
||||
return types.NewImageManifest(ref, digest.Digest("sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d62abcd"), types.Image{OS: "linux", Architecture: "amd64"}, man)
|
||||
}
|
||||
|
||||
func TestInspectCommandLocalManifestNotFound(t *testing.T) {
|
||||
store, cleanup := newTempManifestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
cli := test.NewFakeCli(nil)
|
||||
cli.SetManifestStore(store)
|
||||
|
||||
cmd := newInspectCommand(cli)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"})
|
||||
err := cmd.Execute()
|
||||
assert.EqualError(t, err, "No such manifest: example.com/alpine:3.0")
|
||||
}
|
||||
|
||||
func TestInspectCommandNotFound(t *testing.T) {
|
||||
store, cleanup := newTempManifestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
cli := test.NewFakeCli(nil)
|
||||
cli.SetManifestStore(store)
|
||||
cli.SetRegistryClient(&fakeRegistryClient{
|
||||
getManifestFunc: func(_ context.Context, _ reference.Named) (manifesttypes.ImageManifest, error) {
|
||||
return manifesttypes.ImageManifest{}, errors.New("missing")
|
||||
},
|
||||
getManifestListFunc: func(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error) {
|
||||
return nil, errors.Errorf("No such manifest: %s", ref)
|
||||
},
|
||||
})
|
||||
|
||||
cmd := newInspectCommand(cli)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs([]string{"example.com/alpine:3.0"})
|
||||
err := cmd.Execute()
|
||||
assert.EqualError(t, err, "No such manifest: example.com/alpine:3.0")
|
||||
}
|
||||
|
||||
func TestInspectCommandLocalManifest(t *testing.T) {
|
||||
store, cleanup := newTempManifestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
cli := test.NewFakeCli(nil)
|
||||
cli.SetManifestStore(store)
|
||||
namedRef := ref(t, "alpine:3.0")
|
||||
imageManifest := fullImageManifest(t, namedRef)
|
||||
err := store.Save(ref(t, "list:v1"), namedRef, imageManifest)
|
||||
require.NoError(t, err)
|
||||
|
||||
cmd := newInspectCommand(cli)
|
||||
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"})
|
||||
require.NoError(t, cmd.Execute())
|
||||
actual := cli.OutBuffer()
|
||||
expected := golden.Get(t, "inspect-manifest.golden")
|
||||
assert.Equal(t, string(expected), actual.String())
|
||||
}
|
||||
|
||||
func TestInspectcommandRemoteManifest(t *testing.T) {
|
||||
store, cleanup := newTempManifestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
cli := test.NewFakeCli(nil)
|
||||
cli.SetManifestStore(store)
|
||||
cli.SetRegistryClient(&fakeRegistryClient{
|
||||
getManifestFunc: func(_ context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) {
|
||||
return fullImageManifest(t, ref), nil
|
||||
},
|
||||
})
|
||||
|
||||
cmd := newInspectCommand(cli)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs([]string{"example.com/alpine:3.0"})
|
||||
require.NoError(t, cmd.Execute())
|
||||
actual := cli.OutBuffer()
|
||||
expected := golden.Get(t, "inspect-manifest.golden")
|
||||
assert.Equal(t, string(expected), actual.String())
|
||||
}
|
||||
@@ -1,273 +0,0 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/manifest/types"
|
||||
registryclient "github.com/docker/cli/cli/registry/client"
|
||||
"github.com/docker/distribution/manifest/manifestlist"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type pushOpts struct {
|
||||
insecure bool
|
||||
purge bool
|
||||
target string
|
||||
}
|
||||
|
||||
type mountRequest struct {
|
||||
ref reference.Named
|
||||
manifest types.ImageManifest
|
||||
}
|
||||
|
||||
type manifestBlob struct {
|
||||
canonical reference.Canonical
|
||||
os string
|
||||
}
|
||||
|
||||
type pushRequest struct {
|
||||
targetRef reference.Named
|
||||
list *manifestlist.DeserializedManifestList
|
||||
mountRequests []mountRequest
|
||||
manifestBlobs []manifestBlob
|
||||
insecure bool
|
||||
}
|
||||
|
||||
func newPushListCommand(dockerCli command.Cli) *cobra.Command {
|
||||
opts := pushOpts{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "push [OPTIONS] MANIFEST_LIST",
|
||||
Short: "Push a manifest list to a repository",
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.target = args[0]
|
||||
return runPush(dockerCli, opts)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.purge, "purge", "p", false, "Remove the local manifest list after push")
|
||||
flags.BoolVar(&opts.insecure, "insecure", false, "Allow push to an insecure registry")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runPush(dockerCli command.Cli, opts pushOpts) error {
|
||||
|
||||
targetRef, err := normalizeReference(opts.target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manifests, err := dockerCli.ManifestStore().GetList(targetRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(manifests) == 0 {
|
||||
return errors.Errorf("%s not found", targetRef)
|
||||
}
|
||||
|
||||
pushRequest, err := buildPushRequest(manifests, targetRef, opts.insecure)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
if err := pushList(ctx, dockerCli, pushRequest); err != nil {
|
||||
return err
|
||||
}
|
||||
if opts.purge {
|
||||
return dockerCli.ManifestStore().Remove(targetRef)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildPushRequest(manifests []types.ImageManifest, targetRef reference.Named, insecure bool) (pushRequest, error) {
|
||||
req := pushRequest{targetRef: targetRef, insecure: insecure}
|
||||
|
||||
var err error
|
||||
req.list, err = buildManifestList(manifests, targetRef)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
|
||||
targetRepo, err := registry.ParseRepositoryInfo(targetRef)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
targetRepoName, err := registryclient.RepoNameForReference(targetRepo.Name)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
|
||||
for _, imageManifest := range manifests {
|
||||
manifestRepoName, err := registryclient.RepoNameForReference(imageManifest.Ref)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
|
||||
repoName, _ := reference.WithName(manifestRepoName)
|
||||
if repoName.Name() != targetRepoName {
|
||||
blobs, err := buildBlobRequestList(imageManifest, repoName)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
req.manifestBlobs = append(req.manifestBlobs, blobs...)
|
||||
|
||||
manifestPush, err := buildPutManifestRequest(imageManifest, targetRef)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
req.mountRequests = append(req.mountRequests, manifestPush)
|
||||
}
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func buildManifestList(manifests []types.ImageManifest, targetRef reference.Named) (*manifestlist.DeserializedManifestList, error) {
|
||||
targetRepoInfo, err := registry.ParseRepositoryInfo(targetRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
descriptors := []manifestlist.ManifestDescriptor{}
|
||||
for _, imageManifest := range manifests {
|
||||
if imageManifest.Platform.Architecture == "" || imageManifest.Platform.OS == "" {
|
||||
return nil, errors.Errorf(
|
||||
"manifest %s must have an OS and Architecture to be pushed to a registry", imageManifest.Ref)
|
||||
}
|
||||
descriptor, err := buildManifestDescriptor(targetRepoInfo, imageManifest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
descriptors = append(descriptors, descriptor)
|
||||
}
|
||||
|
||||
return manifestlist.FromDescriptors(descriptors)
|
||||
}
|
||||
|
||||
func buildManifestDescriptor(targetRepo *registry.RepositoryInfo, imageManifest types.ImageManifest) (manifestlist.ManifestDescriptor, error) {
|
||||
repoInfo, err := registry.ParseRepositoryInfo(imageManifest.Ref)
|
||||
if err != nil {
|
||||
return manifestlist.ManifestDescriptor{}, err
|
||||
}
|
||||
|
||||
manifestRepoHostname := reference.Domain(repoInfo.Name)
|
||||
targetRepoHostname := reference.Domain(targetRepo.Name)
|
||||
if manifestRepoHostname != targetRepoHostname {
|
||||
return manifestlist.ManifestDescriptor{}, errors.Errorf("cannot use source images from a different registry than the target image: %s != %s", manifestRepoHostname, targetRepoHostname)
|
||||
}
|
||||
|
||||
mediaType, raw, err := imageManifest.Payload()
|
||||
if err != nil {
|
||||
return manifestlist.ManifestDescriptor{}, err
|
||||
}
|
||||
|
||||
manifest := manifestlist.ManifestDescriptor{
|
||||
Platform: imageManifest.Platform,
|
||||
}
|
||||
manifest.Descriptor.Digest = imageManifest.Digest
|
||||
manifest.Size = int64(len(raw))
|
||||
manifest.MediaType = mediaType
|
||||
|
||||
if err = manifest.Descriptor.Digest.Validate(); err != nil {
|
||||
return manifestlist.ManifestDescriptor{}, errors.Wrapf(err,
|
||||
"digest parse of image %q failed", imageManifest.Ref)
|
||||
}
|
||||
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
func buildBlobRequestList(imageManifest types.ImageManifest, repoName reference.Named) ([]manifestBlob, error) {
|
||||
var blobReqs []manifestBlob
|
||||
|
||||
for _, blobDigest := range imageManifest.Blobs() {
|
||||
canonical, err := reference.WithDigest(repoName, blobDigest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blobReqs = append(blobReqs, manifestBlob{canonical: canonical, os: imageManifest.Platform.OS})
|
||||
}
|
||||
return blobReqs, nil
|
||||
}
|
||||
|
||||
// nolint: interfacer
|
||||
func buildPutManifestRequest(imageManifest types.ImageManifest, targetRef reference.Named) (mountRequest, error) {
|
||||
refWithoutTag, err := reference.WithName(targetRef.Name())
|
||||
if err != nil {
|
||||
return mountRequest{}, err
|
||||
}
|
||||
mountRef, err := reference.WithDigest(refWithoutTag, imageManifest.Digest)
|
||||
if err != nil {
|
||||
return mountRequest{}, err
|
||||
}
|
||||
|
||||
// This indentation has to be added to ensure sha parity with the registry
|
||||
v2ManifestBytes, err := json.MarshalIndent(imageManifest.SchemaV2Manifest, "", " ")
|
||||
if err != nil {
|
||||
return mountRequest{}, err
|
||||
}
|
||||
// indent only the DeserializedManifest portion of this, in order to maintain parity with the registry
|
||||
// and not alter the sha
|
||||
var v2Manifest schema2.DeserializedManifest
|
||||
if err = v2Manifest.UnmarshalJSON(v2ManifestBytes); err != nil {
|
||||
return mountRequest{}, err
|
||||
}
|
||||
imageManifest.SchemaV2Manifest = &v2Manifest
|
||||
|
||||
return mountRequest{ref: mountRef, manifest: imageManifest}, err
|
||||
}
|
||||
|
||||
func pushList(ctx context.Context, dockerCli command.Cli, req pushRequest) error {
|
||||
rclient := dockerCli.RegistryClient(req.insecure)
|
||||
|
||||
if err := mountBlobs(ctx, rclient, req.targetRef, req.manifestBlobs); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := pushReferences(ctx, dockerCli.Out(), rclient, req.mountRequests); err != nil {
|
||||
return err
|
||||
}
|
||||
dgst, err := rclient.PutManifest(ctx, req.targetRef, req.list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(dockerCli.Out(), dgst.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
func pushReferences(ctx context.Context, out io.Writer, client registryclient.RegistryClient, mounts []mountRequest) error {
|
||||
for _, mount := range mounts {
|
||||
newDigest, err := client.PutManifest(ctx, mount.ref, mount.manifest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(out, "Pushed ref %s with digest: %s\n", mount.ref, newDigest)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mountBlobs(ctx context.Context, client registryclient.RegistryClient, ref reference.Named, blobs []manifestBlob) error {
|
||||
for _, blob := range blobs {
|
||||
err := client.MountBlob(ctx, blob.canonical, ref)
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
case registryclient.ErrBlobCreated:
|
||||
if blob.os != "windows" {
|
||||
return fmt.Errorf("error mounting %s to %s", blob.canonical, ref)
|
||||
}
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
manifesttypes "github.com/docker/cli/cli/manifest/types"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/testutil"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func newFakeRegistryClient(t *testing.T) *fakeRegistryClient {
|
||||
require.NoError(t, nil)
|
||||
|
||||
return &fakeRegistryClient{
|
||||
getManifestFunc: func(_ context.Context, _ reference.Named) (manifesttypes.ImageManifest, error) {
|
||||
return manifesttypes.ImageManifest{}, errors.New("")
|
||||
},
|
||||
getManifestListFunc: func(_ context.Context, _ reference.Named) ([]manifesttypes.ImageManifest, error) {
|
||||
return nil, errors.Errorf("")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestManifestPushErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
args: []string{"one-arg", "extra-arg"},
|
||||
expectedError: "requires exactly 1 argument",
|
||||
},
|
||||
{
|
||||
args: []string{"th!si'sa/fa!ke/li$t/-name"},
|
||||
expectedError: "invalid reference format",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(nil)
|
||||
cmd := newPushListCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
// store a one-image manifest list and puah it
|
||||
func TestManifestPush(t *testing.T) {
|
||||
store, sCleanup := newTempManifestStore(t)
|
||||
defer sCleanup()
|
||||
|
||||
registry := newFakeRegistryClient(t)
|
||||
|
||||
cli := test.NewFakeCli(nil)
|
||||
cli.SetManifestStore(store)
|
||||
cli.SetRegistryClient(registry)
|
||||
|
||||
namedRef := ref(t, "alpine:3.0")
|
||||
imageManifest := fullImageManifest(t, namedRef)
|
||||
err := store.Save(ref(t, "list:v1"), namedRef, imageManifest)
|
||||
require.NoError(t, err)
|
||||
|
||||
cmd := newPushListCommand(cli)
|
||||
cmd.SetArgs([]string{"example.com/list:v1"})
|
||||
err = cmd.Execute()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"Ref": "example.com/alpine:3.0",
|
||||
"Digest": "sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d62abcd",
|
||||
"SchemaV2Manifest": {
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"config": {
|
||||
"mediaType": "application/vnd.docker.container.image.v1+json",
|
||||
"size": 1520,
|
||||
"digest": "sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d624560"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 1990402,
|
||||
"digest": "sha256:88286f41530e93dffd4b964e1db22ce4939fffa4a4c665dab8591fbab03d4926"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Platform": {
|
||||
"architecture": "arm",
|
||||
"os": "freebsd",
|
||||
"os.features": [
|
||||
"feature1"
|
||||
],
|
||||
"variant": "v7"
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
|
||||
"manifests": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"size": 428,
|
||||
"digest": "sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d62abcd",
|
||||
"platform": {
|
||||
"architecture": "amd64",
|
||||
"os": "linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"size": 428,
|
||||
"digest": "sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d62abcd",
|
||||
"platform": {
|
||||
"architecture": "amd64",
|
||||
"os": "linux"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"config": {
|
||||
"mediaType": "application/vnd.docker.container.image.v1+json",
|
||||
"size": 1520,
|
||||
"digest": "sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d624560"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 1990402,
|
||||
"digest": "sha256:88286f41530e93dffd4b964e1db22ce4939fffa4a4c665dab8591fbab03d4926"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/manifest/store"
|
||||
"github.com/docker/cli/cli/manifest/types"
|
||||
"github.com/docker/distribution/reference"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type osArch struct {
|
||||
os string
|
||||
arch string
|
||||
}
|
||||
|
||||
// Remove any unsupported os/arch combo
|
||||
// list of valid os/arch values (see "Optional Environment Variables" section
|
||||
// of https://golang.org/doc/install/source
|
||||
// Added linux/s390x as we know System z support already exists
|
||||
var validOSArches = map[osArch]bool{
|
||||
{os: "darwin", arch: "386"}: true,
|
||||
{os: "darwin", arch: "amd64"}: true,
|
||||
{os: "darwin", arch: "arm"}: true,
|
||||
{os: "darwin", arch: "arm64"}: true,
|
||||
{os: "dragonfly", arch: "amd64"}: true,
|
||||
{os: "freebsd", arch: "386"}: true,
|
||||
{os: "freebsd", arch: "amd64"}: true,
|
||||
{os: "freebsd", arch: "arm"}: true,
|
||||
{os: "linux", arch: "386"}: true,
|
||||
{os: "linux", arch: "amd64"}: true,
|
||||
{os: "linux", arch: "arm"}: true,
|
||||
{os: "linux", arch: "arm64"}: true,
|
||||
{os: "linux", arch: "ppc64le"}: true,
|
||||
{os: "linux", arch: "mips64"}: true,
|
||||
{os: "linux", arch: "mips64le"}: true,
|
||||
{os: "linux", arch: "s390x"}: true,
|
||||
{os: "netbsd", arch: "386"}: true,
|
||||
{os: "netbsd", arch: "amd64"}: true,
|
||||
{os: "netbsd", arch: "arm"}: true,
|
||||
{os: "openbsd", arch: "386"}: true,
|
||||
{os: "openbsd", arch: "amd64"}: true,
|
||||
{os: "openbsd", arch: "arm"}: true,
|
||||
{os: "plan9", arch: "386"}: true,
|
||||
{os: "plan9", arch: "amd64"}: true,
|
||||
{os: "solaris", arch: "amd64"}: true,
|
||||
{os: "windows", arch: "386"}: true,
|
||||
{os: "windows", arch: "amd64"}: true,
|
||||
}
|
||||
|
||||
func isValidOSArch(os string, arch string) bool {
|
||||
// check for existence of this combo
|
||||
_, ok := validOSArches[osArch{os, arch}]
|
||||
return ok
|
||||
}
|
||||
|
||||
func normalizeReference(ref string) (reference.Named, error) {
|
||||
namedRef, err := reference.ParseNormalizedNamed(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, isDigested := namedRef.(reference.Canonical); !isDigested {
|
||||
return reference.TagNameOnly(namedRef), nil
|
||||
}
|
||||
return namedRef, nil
|
||||
}
|
||||
|
||||
// getManifest from the local store, and fallback to the remote registry if it
|
||||
// doesn't exist locally
|
||||
func getManifest(ctx context.Context, dockerCli command.Cli, listRef, namedRef reference.Named, insecure bool) (types.ImageManifest, error) {
|
||||
data, err := dockerCli.ManifestStore().Get(listRef, namedRef)
|
||||
switch {
|
||||
case store.IsNotFound(err):
|
||||
return dockerCli.RegistryClient(insecure).GetManifest(ctx, namedRef)
|
||||
case err != nil:
|
||||
return types.ImageManifest{}, err
|
||||
default:
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
@@ -21,9 +21,9 @@ const (
|
||||
|
||||
func normalize(flag string) Orchestrator {
|
||||
switch flag {
|
||||
case "kubernetes":
|
||||
case "kubernetes", "k8s":
|
||||
return OrchestratorKubernetes
|
||||
case "swarm":
|
||||
case "swarm", "swarmkit":
|
||||
return OrchestratorSwarm
|
||||
default:
|
||||
return orchestratorUnset
|
||||
|
||||
@@ -10,13 +10,14 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/api/types"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/pkg/term"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// ElectAuthServer returns the default registry to use (by asking the daemon)
|
||||
|
||||
@@ -16,7 +16,6 @@ type fakeClient struct {
|
||||
serviceListFunc func(context.Context, types.ServiceListOptions) ([]swarm.Service, error)
|
||||
taskListFunc func(context.Context, types.TaskListOptions) ([]swarm.Task, error)
|
||||
infoFunc func(ctx context.Context) (types.Info, error)
|
||||
networkInspectFunc func(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, error)
|
||||
}
|
||||
|
||||
func (f *fakeClient) NodeList(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error) {
|
||||
@@ -61,13 +60,6 @@ func (f *fakeClient) Info(ctx context.Context) (types.Info, error) {
|
||||
return f.infoFunc(ctx)
|
||||
}
|
||||
|
||||
func (f *fakeClient) NetworkInspect(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, error) {
|
||||
if f.networkInspectFunc != nil {
|
||||
return f.networkInspectFunc(ctx, networkID, options)
|
||||
}
|
||||
return types.NetworkResource{}, nil
|
||||
}
|
||||
|
||||
func newService(id string, name string) swarm.Service {
|
||||
return swarm.Service{
|
||||
ID: id,
|
||||
|
||||
@@ -353,21 +353,22 @@ func (c *credentialSpecOpt) Value() *swarm.CredentialSpec {
|
||||
return c.value
|
||||
}
|
||||
|
||||
func resolveNetworkID(ctx context.Context, apiClient client.NetworkAPIClient, networkIDOrName string) (string, error) {
|
||||
nw, err := apiClient.NetworkInspect(ctx, networkIDOrName, types.NetworkInspectOptions{Scope: "swarm"})
|
||||
return nw.ID, err
|
||||
}
|
||||
|
||||
func convertNetworks(networks opts.NetworkOpt) []swarm.NetworkAttachmentConfig {
|
||||
func convertNetworks(ctx context.Context, apiClient client.NetworkAPIClient, networks opts.NetworkOpt) ([]swarm.NetworkAttachmentConfig, error) {
|
||||
var netAttach []swarm.NetworkAttachmentConfig
|
||||
for _, net := range networks.Value() {
|
||||
networkIDOrName := net.Target
|
||||
_, err := apiClient.NetworkInspect(ctx, networkIDOrName, types.NetworkInspectOptions{Scope: "swarm"})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
netAttach = append(netAttach, swarm.NetworkAttachmentConfig{
|
||||
Target: net.Target,
|
||||
Aliases: net.Aliases,
|
||||
DriverOpts: net.DriverOpts,
|
||||
})
|
||||
}
|
||||
return netAttach
|
||||
sort.Sort(byNetworkTarget(netAttach))
|
||||
return netAttach, nil
|
||||
}
|
||||
|
||||
type endpointOptions struct {
|
||||
@@ -589,15 +590,10 @@ func (options *serviceOptions) ToService(ctx context.Context, apiClient client.N
|
||||
return service, err
|
||||
}
|
||||
|
||||
networks := convertNetworks(options.networks)
|
||||
for i, net := range networks {
|
||||
nwID, err := resolveNetworkID(ctx, apiClient, net.Target)
|
||||
if err != nil {
|
||||
return service, err
|
||||
}
|
||||
networks[i].Target = nwID
|
||||
networks, err := convertNetworks(ctx, apiClient, options.networks)
|
||||
if err != nil {
|
||||
return service, err
|
||||
}
|
||||
sort.Sort(byNetworkTarget(networks))
|
||||
|
||||
resources, err := options.resources.ToResourceRequirements()
|
||||
if err != nil {
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMemBytesString(t *testing.T) {
|
||||
@@ -128,37 +123,3 @@ func TestResourceOptionsToResourceRequirements(t *testing.T) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestToServiceNetwork(t *testing.T) {
|
||||
nws := []types.NetworkResource{
|
||||
{Name: "aaa-network", ID: "id555"},
|
||||
{Name: "mmm-network", ID: "id999"},
|
||||
{Name: "zzz-network", ID: "id111"},
|
||||
}
|
||||
|
||||
client := &fakeClient{
|
||||
networkInspectFunc: func(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, error) {
|
||||
for _, network := range nws {
|
||||
if network.ID == networkID || network.Name == networkID {
|
||||
return network, nil
|
||||
}
|
||||
}
|
||||
return types.NetworkResource{}, fmt.Errorf("network not found: %s", networkID)
|
||||
},
|
||||
}
|
||||
|
||||
nwo := opts.NetworkOpt{}
|
||||
nwo.Set("zzz-network")
|
||||
nwo.Set("mmm-network")
|
||||
nwo.Set("aaa-network")
|
||||
|
||||
o := newServiceOptions()
|
||||
o.mode = "replicated"
|
||||
o.networks = nwo
|
||||
|
||||
ctx := context.Background()
|
||||
flags := newCreateCommand(nil).Flags()
|
||||
service, err := o.ToService(ctx, client, flags)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []swarm.NetworkAttachmentConfig{{Target: "id111"}, {Target: "id555"}, {Target: "id999"}}, service.TaskTemplate.Networks)
|
||||
}
|
||||
|
||||
@@ -1119,16 +1119,14 @@ func updateNetworks(ctx context.Context, apiClient client.NetworkAPIClient, flag
|
||||
|
||||
if flags.Changed(flagNetworkAdd) {
|
||||
values := flags.Lookup(flagNetworkAdd).Value.(*opts.NetworkOpt)
|
||||
networks := convertNetworks(*values)
|
||||
networks, err := convertNetworks(ctx, apiClient, *values)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, network := range networks {
|
||||
nwID, err := resolveNetworkID(ctx, apiClient, network.Target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, exists := existingNetworks[nwID]; exists {
|
||||
if _, exists := existingNetworks[network.Target]; exists {
|
||||
return errors.Errorf("service is already attached to network %s", network.Target)
|
||||
}
|
||||
network.Target = nwID
|
||||
newNetworks = append(newNetworks, network)
|
||||
existingNetworks[network.Target] = struct{}{}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
@@ -587,69 +586,3 @@ func TestRemoveGenericResources(t *testing.T) {
|
||||
assert.NoError(t, removeGenericResources(flags, task))
|
||||
assert.Len(t, task.Resources.Reservations.GenericResources, 1)
|
||||
}
|
||||
|
||||
func TestUpdateNetworks(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
nws := []types.NetworkResource{
|
||||
{Name: "aaa-network", ID: "id555"},
|
||||
{Name: "mmm-network", ID: "id999"},
|
||||
{Name: "zzz-network", ID: "id111"},
|
||||
}
|
||||
|
||||
client := &fakeClient{
|
||||
networkInspectFunc: func(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, error) {
|
||||
for _, network := range nws {
|
||||
if network.ID == networkID || network.Name == networkID {
|
||||
return network, nil
|
||||
}
|
||||
}
|
||||
return types.NetworkResource{}, fmt.Errorf("network not found: %s", networkID)
|
||||
},
|
||||
}
|
||||
|
||||
svc := swarm.ServiceSpec{
|
||||
TaskTemplate: swarm.TaskSpec{
|
||||
ContainerSpec: &swarm.ContainerSpec{},
|
||||
Networks: []swarm.NetworkAttachmentConfig{
|
||||
{Target: "id999"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
flags := newUpdateCommand(nil).Flags()
|
||||
err := flags.Set(flagNetworkAdd, "aaa-network")
|
||||
require.NoError(t, err)
|
||||
err = updateService(ctx, client, flags, &svc)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []swarm.NetworkAttachmentConfig{{Target: "id555"}, {Target: "id999"}}, svc.TaskTemplate.Networks)
|
||||
|
||||
flags = newUpdateCommand(nil).Flags()
|
||||
err = flags.Set(flagNetworkAdd, "aaa-network")
|
||||
require.NoError(t, err)
|
||||
err = updateService(ctx, client, flags, &svc)
|
||||
assert.EqualError(t, err, "service is already attached to network aaa-network")
|
||||
assert.Equal(t, []swarm.NetworkAttachmentConfig{{Target: "id555"}, {Target: "id999"}}, svc.TaskTemplate.Networks)
|
||||
|
||||
flags = newUpdateCommand(nil).Flags()
|
||||
err = flags.Set(flagNetworkAdd, "id555")
|
||||
require.NoError(t, err)
|
||||
err = updateService(ctx, client, flags, &svc)
|
||||
assert.EqualError(t, err, "service is already attached to network id555")
|
||||
assert.Equal(t, []swarm.NetworkAttachmentConfig{{Target: "id555"}, {Target: "id999"}}, svc.TaskTemplate.Networks)
|
||||
|
||||
flags = newUpdateCommand(nil).Flags()
|
||||
err = flags.Set(flagNetworkRemove, "id999")
|
||||
require.NoError(t, err)
|
||||
err = updateService(ctx, client, flags, &svc)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []swarm.NetworkAttachmentConfig{{Target: "id555"}}, svc.TaskTemplate.Networks)
|
||||
|
||||
flags = newUpdateCommand(nil).Flags()
|
||||
err = flags.Set(flagNetworkAdd, "mmm-network")
|
||||
require.NoError(t, err)
|
||||
err = flags.Set(flagNetworkRemove, "aaa-network")
|
||||
require.NoError(t, err)
|
||||
err = updateService(ctx, client, flags, &svc)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []swarm.NetworkAttachmentConfig{{Target: "id999"}}, svc.TaskTemplate.Networks)
|
||||
}
|
||||
|
||||
@@ -243,5 +243,8 @@ func addStagedSigner(notaryRepo client.Repository, newSigner data.RoleName, sign
|
||||
if err := notaryRepo.AddDelegationRoleAndKeys(trust.ReleasesRole, signerKeys); err != nil {
|
||||
return err
|
||||
}
|
||||
return notaryRepo.AddDelegationPaths(trust.ReleasesRole, []string{""})
|
||||
if err := notaryRepo.AddDelegationPaths(trust.ReleasesRole, []string{""}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -131,5 +131,8 @@ func removeSingleSigner(cli command.Cli, repoName, signerName string, forceYes b
|
||||
if err = notaryRepo.RemoveDelegationRole(signerDelegation); err != nil {
|
||||
return err
|
||||
}
|
||||
return notaryRepo.Publish()
|
||||
if err = notaryRepo.Publish(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -22,37 +22,43 @@ func Volumes(serviceVolumes []composetypes.ServiceVolumeConfig, stackVolumes vol
|
||||
return mounts, nil
|
||||
}
|
||||
|
||||
func createMountFromVolume(volume composetypes.ServiceVolumeConfig) mount.Mount {
|
||||
return mount.Mount{
|
||||
Type: mount.Type(volume.Type),
|
||||
Target: volume.Target,
|
||||
ReadOnly: volume.ReadOnly,
|
||||
Source: volume.Source,
|
||||
Consistency: mount.Consistency(volume.Consistency),
|
||||
}
|
||||
}
|
||||
|
||||
func handleVolumeToMount(
|
||||
func convertVolumeToMount(
|
||||
volume composetypes.ServiceVolumeConfig,
|
||||
stackVolumes volumes,
|
||||
namespace Namespace,
|
||||
) (mount.Mount, error) {
|
||||
result := createMountFromVolume(volume)
|
||||
result := mount.Mount{
|
||||
Type: mount.Type(volume.Type),
|
||||
Source: volume.Source,
|
||||
Target: volume.Target,
|
||||
ReadOnly: volume.ReadOnly,
|
||||
Consistency: mount.Consistency(volume.Consistency),
|
||||
}
|
||||
|
||||
if volume.Tmpfs != nil {
|
||||
return mount.Mount{}, errors.New("tmpfs options are incompatible with type volume")
|
||||
}
|
||||
if volume.Bind != nil {
|
||||
return mount.Mount{}, errors.New("bind options are incompatible with type volume")
|
||||
}
|
||||
// Anonymous volumes
|
||||
if volume.Source == "" {
|
||||
return result, nil
|
||||
}
|
||||
if volume.Type == "volume" && volume.Bind != nil {
|
||||
return result, errors.New("bind options are incompatible with type volume")
|
||||
}
|
||||
if volume.Type == "bind" && volume.Volume != nil {
|
||||
return result, errors.New("volume options are incompatible with type bind")
|
||||
}
|
||||
|
||||
if volume.Bind != nil {
|
||||
result.BindOptions = &mount.BindOptions{
|
||||
Propagation: mount.Propagation(volume.Bind.Propagation),
|
||||
}
|
||||
}
|
||||
// Binds volumes
|
||||
if volume.Type == "bind" {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
stackVolume, exists := stackVolumes[volume.Source]
|
||||
if !exists {
|
||||
return mount.Mount{}, errors.Errorf("undefined volume %q", volume.Source)
|
||||
return result, errors.Errorf("undefined volume %q", volume.Source)
|
||||
}
|
||||
|
||||
result.Source = namespace.Scope(volume.Source)
|
||||
@@ -79,62 +85,6 @@ func handleVolumeToMount(
|
||||
}
|
||||
}
|
||||
|
||||
// Named volumes
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func handleBindToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, error) {
|
||||
result := createMountFromVolume(volume)
|
||||
|
||||
if volume.Source == "" {
|
||||
return mount.Mount{}, errors.New("invalid bind source, source cannot be empty")
|
||||
}
|
||||
if volume.Volume != nil {
|
||||
return mount.Mount{}, errors.New("volume options are incompatible with type bind")
|
||||
}
|
||||
if volume.Tmpfs != nil {
|
||||
return mount.Mount{}, errors.New("tmpfs options are incompatible with type bind")
|
||||
}
|
||||
if volume.Bind != nil {
|
||||
result.BindOptions = &mount.BindOptions{
|
||||
Propagation: mount.Propagation(volume.Bind.Propagation),
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func handleTmpfsToMount(volume composetypes.ServiceVolumeConfig) (mount.Mount, error) {
|
||||
result := createMountFromVolume(volume)
|
||||
|
||||
if volume.Source != "" {
|
||||
return mount.Mount{}, errors.New("invalid tmpfs source, source must be empty")
|
||||
}
|
||||
if volume.Bind != nil {
|
||||
return mount.Mount{}, errors.New("bind options are incompatible with type tmpfs")
|
||||
}
|
||||
if volume.Volume != nil {
|
||||
return mount.Mount{}, errors.New("volume options are incompatible with type tmpfs")
|
||||
}
|
||||
if volume.Tmpfs != nil {
|
||||
result.TmpfsOptions = &mount.TmpfsOptions{
|
||||
SizeBytes: volume.Tmpfs.Size,
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func convertVolumeToMount(
|
||||
volume composetypes.ServiceVolumeConfig,
|
||||
stackVolumes volumes,
|
||||
namespace Namespace,
|
||||
) (mount.Mount, error) {
|
||||
|
||||
switch volume.Type {
|
||||
case "volume", "":
|
||||
return handleVolumeToMount(volume, stackVolumes, namespace)
|
||||
case "bind":
|
||||
return handleBindToMount(volume)
|
||||
case "tmpfs":
|
||||
return handleTmpfsToMount(volume)
|
||||
}
|
||||
return mount.Mount{}, errors.New("volume type must be volume, bind, or tmpfs")
|
||||
}
|
||||
|
||||
@@ -22,28 +22,7 @@ func TestConvertVolumeToMountAnonymousVolume(t *testing.T) {
|
||||
assert.Equal(t, expected, mount)
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountAnonymousBind(t *testing.T) {
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "bind",
|
||||
Target: "/foo/bar",
|
||||
Bind: &composetypes.ServiceVolumeBind{
|
||||
Propagation: "slave",
|
||||
},
|
||||
}
|
||||
_, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo"))
|
||||
assert.EqualError(t, err, "invalid bind source, source cannot be empty")
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountUnapprovedType(t *testing.T) {
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "foo",
|
||||
Target: "/foo/bar",
|
||||
}
|
||||
_, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo"))
|
||||
assert.EqualError(t, err, "volume type must be volume, bind, or tmpfs")
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountConflictingOptionsBindInVolume(t *testing.T) {
|
||||
func TestConvertVolumeToMountConflictingOptionsBind(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
@@ -58,22 +37,7 @@ func TestConvertVolumeToMountConflictingOptionsBindInVolume(t *testing.T) {
|
||||
assert.EqualError(t, err, "bind options are incompatible with type volume")
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountConflictingOptionsTmpfsInVolume(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "volume",
|
||||
Source: "foo",
|
||||
Target: "/target",
|
||||
Tmpfs: &composetypes.ServiceVolumeTmpfs{
|
||||
Size: 1000,
|
||||
},
|
||||
}
|
||||
_, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||
assert.EqualError(t, err, "tmpfs options are incompatible with type volume")
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountConflictingOptionsVolumeInBind(t *testing.T) {
|
||||
func TestConvertVolumeToMountConflictingOptionsVolume(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
@@ -88,49 +52,6 @@ func TestConvertVolumeToMountConflictingOptionsVolumeInBind(t *testing.T) {
|
||||
assert.EqualError(t, err, "volume options are incompatible with type bind")
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountConflictingOptionsTmpfsInBind(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "bind",
|
||||
Source: "/foo",
|
||||
Target: "/target",
|
||||
Tmpfs: &composetypes.ServiceVolumeTmpfs{
|
||||
Size: 1000,
|
||||
},
|
||||
}
|
||||
_, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||
assert.EqualError(t, err, "tmpfs options are incompatible with type bind")
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountConflictingOptionsBindInTmpfs(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "tmpfs",
|
||||
Target: "/target",
|
||||
Bind: &composetypes.ServiceVolumeBind{
|
||||
Propagation: "slave",
|
||||
},
|
||||
}
|
||||
_, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||
assert.EqualError(t, err, "bind options are incompatible with type tmpfs")
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountConflictingOptionsVolumeInTmpfs(t *testing.T) {
|
||||
namespace := NewNamespace("foo")
|
||||
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "tmpfs",
|
||||
Target: "/target",
|
||||
Volume: &composetypes.ServiceVolumeVolume{
|
||||
NoCopy: true,
|
||||
},
|
||||
}
|
||||
_, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||
assert.EqualError(t, err, "volume options are incompatible with type tmpfs")
|
||||
}
|
||||
|
||||
func TestConvertVolumeToMountNamedVolume(t *testing.T) {
|
||||
stackVolumes := volumes{
|
||||
"normal": composetypes.VolumeConfig{
|
||||
@@ -310,35 +231,3 @@ func TestConvertVolumeToMountVolumeDoesNotExist(t *testing.T) {
|
||||
_, err := convertVolumeToMount(config, volumes{}, namespace)
|
||||
assert.EqualError(t, err, "undefined volume \"unknown\"")
|
||||
}
|
||||
|
||||
func TestConvertTmpfsToMountVolume(t *testing.T) {
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "tmpfs",
|
||||
Target: "/foo/bar",
|
||||
Tmpfs: &composetypes.ServiceVolumeTmpfs{
|
||||
Size: 1000,
|
||||
},
|
||||
}
|
||||
expected := mount.Mount{
|
||||
Type: mount.TypeTmpfs,
|
||||
Target: "/foo/bar",
|
||||
TmpfsOptions: &mount.TmpfsOptions{SizeBytes: 1000},
|
||||
}
|
||||
mount, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo"))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, mount)
|
||||
}
|
||||
|
||||
func TestConvertTmpfsToMountVolumeWithSource(t *testing.T) {
|
||||
config := composetypes.ServiceVolumeConfig{
|
||||
Type: "tmpfs",
|
||||
Source: "/bar",
|
||||
Target: "/foo/bar",
|
||||
Tmpfs: &composetypes.ServiceVolumeTmpfs{
|
||||
Size: 1000,
|
||||
},
|
||||
}
|
||||
|
||||
_, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo"))
|
||||
assert.EqualError(t, err, "invalid tmpfs source, source must be empty")
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
version: "3.6"
|
||||
version: "3.5"
|
||||
|
||||
services:
|
||||
foo:
|
||||
@@ -10,7 +10,7 @@ services:
|
||||
foo: bar
|
||||
target: foo
|
||||
network: foo
|
||||
cache_from:
|
||||
cache_from:
|
||||
- foo
|
||||
- bar
|
||||
labels: [FOO=BAR]
|
||||
@@ -249,10 +249,6 @@ services:
|
||||
source: ./opt
|
||||
target: /opt
|
||||
consistency: cached
|
||||
- type: tmpfs
|
||||
target: /opt
|
||||
tmpfs:
|
||||
size: 10000
|
||||
|
||||
working_dir: /code
|
||||
|
||||
|
||||
@@ -1149,9 +1149,6 @@ func TestFullExample(t *testing.T) {
|
||||
{Source: homeDir + "/configs", Target: "/etc/configs/", Type: "bind", ReadOnly: true},
|
||||
{Source: "datavolume", Target: "/var/lib/mysql", Type: "volume"},
|
||||
{Source: workingDir + "/opt", Target: "/opt", Consistency: "cached", Type: "bind"},
|
||||
{Target: "/opt", Type: "tmpfs", Tmpfs: &types.ServiceVolumeTmpfs{
|
||||
Size: int64(10000),
|
||||
}},
|
||||
},
|
||||
WorkingDir: "/code",
|
||||
}
|
||||
@@ -1223,106 +1220,6 @@ func TestFullExample(t *testing.T) {
|
||||
assert.Equal(t, expectedVolumeConfig, config.Volumes)
|
||||
}
|
||||
|
||||
func TestLoadTmpfsVolume(t *testing.T) {
|
||||
config, err := loadYAML(`
|
||||
version: "3.6"
|
||||
services:
|
||||
tmpfs:
|
||||
image: nginx:latest
|
||||
volumes:
|
||||
- type: tmpfs
|
||||
target: /app
|
||||
tmpfs:
|
||||
size: 10000
|
||||
`)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := types.ServiceVolumeConfig{
|
||||
Target: "/app",
|
||||
Type: "tmpfs",
|
||||
Tmpfs: &types.ServiceVolumeTmpfs{
|
||||
Size: int64(10000),
|
||||
},
|
||||
}
|
||||
|
||||
require.Len(t, config.Services, 1)
|
||||
assert.Len(t, config.Services[0].Volumes, 1)
|
||||
assert.Equal(t, expected, config.Services[0].Volumes[0])
|
||||
}
|
||||
|
||||
func TestLoadTmpfsVolumeAdditionalPropertyNotAllowed(t *testing.T) {
|
||||
_, err := loadYAML(`
|
||||
version: "3.5"
|
||||
services:
|
||||
tmpfs:
|
||||
image: nginx:latest
|
||||
volumes:
|
||||
- type: tmpfs
|
||||
target: /app
|
||||
tmpfs:
|
||||
size: 10000
|
||||
`)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "services.tmpfs.volumes.0 Additional property tmpfs is not allowed")
|
||||
}
|
||||
|
||||
func TestLoadTmpfsVolumeSizeCanBeZero(t *testing.T) {
|
||||
config, err := loadYAML(`
|
||||
version: "3.6"
|
||||
services:
|
||||
tmpfs:
|
||||
image: nginx:latest
|
||||
volumes:
|
||||
- type: tmpfs
|
||||
target: /app
|
||||
tmpfs:
|
||||
size: 0
|
||||
`)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := types.ServiceVolumeConfig{
|
||||
Target: "/app",
|
||||
Type: "tmpfs",
|
||||
Tmpfs: &types.ServiceVolumeTmpfs{},
|
||||
}
|
||||
|
||||
require.Len(t, config.Services, 1)
|
||||
assert.Len(t, config.Services[0].Volumes, 1)
|
||||
assert.Equal(t, expected, config.Services[0].Volumes[0])
|
||||
}
|
||||
|
||||
func TestLoadTmpfsVolumeSizeMustBeGTEQZero(t *testing.T) {
|
||||
_, err := loadYAML(`
|
||||
version: "3.6"
|
||||
services:
|
||||
tmpfs:
|
||||
image: nginx:latest
|
||||
volumes:
|
||||
- type: tmpfs
|
||||
target: /app
|
||||
tmpfs:
|
||||
size: -1
|
||||
`)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "services.tmpfs.volumes.0.tmpfs.size Must be greater than or equal to 0")
|
||||
}
|
||||
|
||||
func TestLoadTmpfsVolumeSizeMustBeInteger(t *testing.T) {
|
||||
_, err := loadYAML(`
|
||||
version: "3.6"
|
||||
services:
|
||||
tmpfs:
|
||||
image: nginx:latest
|
||||
volumes:
|
||||
- type: tmpfs
|
||||
target: /app
|
||||
tmpfs:
|
||||
size: 0.0001
|
||||
`)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "services.tmpfs.volumes.0.tmpfs.size must be a integer")
|
||||
}
|
||||
|
||||
func serviceSort(services []types.ServiceConfig) []types.ServiceConfig {
|
||||
sort.Sort(servicesByName(services))
|
||||
return services
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,582 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"id": "config_schema_v3.5.json",
|
||||
"type": "object",
|
||||
"required": ["version"],
|
||||
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
"services": {
|
||||
"id": "#/properties/services",
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^[a-zA-Z0-9._-]+$": {
|
||||
"$ref": "#/definitions/service"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
|
||||
"networks": {
|
||||
"id": "#/properties/networks",
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^[a-zA-Z0-9._-]+$": {
|
||||
"$ref": "#/definitions/network"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"volumes": {
|
||||
"id": "#/properties/volumes",
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^[a-zA-Z0-9._-]+$": {
|
||||
"$ref": "#/definitions/volume"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
|
||||
"secrets": {
|
||||
"id": "#/properties/secrets",
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^[a-zA-Z0-9._-]+$": {
|
||||
"$ref": "#/definitions/secret"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
|
||||
"configs": {
|
||||
"id": "#/properties/configs",
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^[a-zA-Z0-9._-]+$": {
|
||||
"$ref": "#/definitions/config"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
|
||||
"patternProperties": {"^x-": {}},
|
||||
"additionalProperties": false,
|
||||
|
||||
"definitions": {
|
||||
|
||||
"service": {
|
||||
"id": "#/definitions/service",
|
||||
"type": "object",
|
||||
|
||||
"properties": {
|
||||
"deploy": {"$ref": "#/definitions/deployment"},
|
||||
"build": {
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"context": {"type": "string"},
|
||||
"dockerfile": {"type": "string"},
|
||||
"args": {"$ref": "#/definitions/list_or_dict"},
|
||||
"labels": {"$ref": "#/definitions/list_or_dict"},
|
||||
"cache_from": {"$ref": "#/definitions/list_of_strings"},
|
||||
"network": {"type": "string"},
|
||||
"target": {"type": "string"},
|
||||
"shm_size": {"type": ["integer", "string"]}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
||||
"cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
||||
"cgroup_parent": {"type": "string"},
|
||||
"command": {
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{"type": "array", "items": {"type": "string"}}
|
||||
]
|
||||
},
|
||||
"configs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"source": {"type": "string"},
|
||||
"target": {"type": "string"},
|
||||
"uid": {"type": "string"},
|
||||
"gid": {"type": "string"},
|
||||
"mode": {"type": "number"}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"container_name": {"type": "string"},
|
||||
"credential_spec": {"type": "object", "properties": {
|
||||
"file": {"type": "string"},
|
||||
"registry": {"type": "string"}
|
||||
}},
|
||||
"depends_on": {"$ref": "#/definitions/list_of_strings"},
|
||||
"devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
||||
"dns": {"$ref": "#/definitions/string_or_list"},
|
||||
"dns_search": {"$ref": "#/definitions/string_or_list"},
|
||||
"domainname": {"type": "string"},
|
||||
"entrypoint": {
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{"type": "array", "items": {"type": "string"}}
|
||||
]
|
||||
},
|
||||
"env_file": {"$ref": "#/definitions/string_or_list"},
|
||||
"environment": {"$ref": "#/definitions/list_or_dict"},
|
||||
|
||||
"expose": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": ["string", "number"],
|
||||
"format": "expose"
|
||||
},
|
||||
"uniqueItems": true
|
||||
},
|
||||
|
||||
"external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
||||
"extra_hosts": {"$ref": "#/definitions/list_or_dict"},
|
||||
"healthcheck": {"$ref": "#/definitions/healthcheck"},
|
||||
"hostname": {"type": "string"},
|
||||
"image": {"type": "string"},
|
||||
"ipc": {"type": "string"},
|
||||
"isolation": {"type": "string"},
|
||||
"labels": {"$ref": "#/definitions/list_or_dict"},
|
||||
"links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
||||
|
||||
"logging": {
|
||||
"type": "object",
|
||||
|
||||
"properties": {
|
||||
"driver": {"type": "string"},
|
||||
"options": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^.+$": {"type": ["string", "number", "null"]}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
|
||||
"mac_address": {"type": "string"},
|
||||
"network_mode": {"type": "string"},
|
||||
|
||||
"networks": {
|
||||
"oneOf": [
|
||||
{"$ref": "#/definitions/list_of_strings"},
|
||||
{
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^[a-zA-Z0-9._-]+$": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"aliases": {"$ref": "#/definitions/list_of_strings"},
|
||||
"ipv4_address": {"type": "string"},
|
||||
"ipv6_address": {"type": "string"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{"type": "null"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"pid": {"type": ["string", "null"]},
|
||||
|
||||
"ports": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{"type": "number", "format": "ports"},
|
||||
{"type": "string", "format": "ports"},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": {"type": "string"},
|
||||
"target": {"type": "integer"},
|
||||
"published": {"type": "integer"},
|
||||
"protocol": {"type": "string"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"uniqueItems": true
|
||||
},
|
||||
|
||||
"privileged": {"type": "boolean"},
|
||||
"read_only": {"type": "boolean"},
|
||||
"restart": {"type": "string"},
|
||||
"security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
||||
"shm_size": {"type": ["number", "string"]},
|
||||
"secrets": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"source": {"type": "string"},
|
||||
"target": {"type": "string"},
|
||||
"uid": {"type": "string"},
|
||||
"gid": {"type": "string"},
|
||||
"mode": {"type": "number"}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"sysctls": {"$ref": "#/definitions/list_or_dict"},
|
||||
"stdin_open": {"type": "boolean"},
|
||||
"stop_grace_period": {"type": "string", "format": "duration"},
|
||||
"stop_signal": {"type": "string"},
|
||||
"tmpfs": {"$ref": "#/definitions/string_or_list"},
|
||||
"tty": {"type": "boolean"},
|
||||
"ulimits": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^[a-z]+$": {
|
||||
"oneOf": [
|
||||
{"type": "integer"},
|
||||
{
|
||||
"type":"object",
|
||||
"properties": {
|
||||
"hard": {"type": "integer"},
|
||||
"soft": {"type": "integer"}
|
||||
},
|
||||
"required": ["soft", "hard"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"user": {"type": "string"},
|
||||
"userns_mode": {"type": "string"},
|
||||
"volumes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["type"],
|
||||
"properties": {
|
||||
"type": {"type": "string"},
|
||||
"source": {"type": "string"},
|
||||
"target": {"type": "string"},
|
||||
"read_only": {"type": "boolean"},
|
||||
"consistency": {"type": "string"},
|
||||
"bind": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"propagation": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"volume": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"nocopy": {"type": "boolean"}
|
||||
}
|
||||
},
|
||||
"tmpfs": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
],
|
||||
"uniqueItems": true
|
||||
}
|
||||
},
|
||||
"working_dir": {"type": "string"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
|
||||
"healthcheck": {
|
||||
"id": "#/definitions/healthcheck",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"disable": {"type": "boolean"},
|
||||
"interval": {"type": "string", "format": "duration"},
|
||||
"retries": {"type": "number"},
|
||||
"test": {
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{"type": "array", "items": {"type": "string"}}
|
||||
]
|
||||
},
|
||||
"timeout": {"type": "string", "format": "duration"},
|
||||
"start_period": {"type": "string", "format": "duration"}
|
||||
}
|
||||
},
|
||||
"deployment": {
|
||||
"id": "#/definitions/deployment",
|
||||
"type": ["object", "null"],
|
||||
"properties": {
|
||||
"mode": {"type": "string"},
|
||||
"endpoint_mode": {"type": "string"},
|
||||
"replicas": {"type": "integer"},
|
||||
"labels": {"$ref": "#/definitions/list_or_dict"},
|
||||
"update_config": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"parallelism": {"type": "integer"},
|
||||
"delay": {"type": "string", "format": "duration"},
|
||||
"failure_action": {"type": "string"},
|
||||
"monitor": {"type": "string", "format": "duration"},
|
||||
"max_failure_ratio": {"type": "number"},
|
||||
"order": {"type": "string", "enum": [
|
||||
"start-first", "stop-first"
|
||||
]}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"resources": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limits": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cpus": {"type": "string"},
|
||||
"memory": {"type": "string"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"reservations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cpus": {"type": "string"},
|
||||
"memory": {"type": "string"},
|
||||
"generic_resources": {"$ref": "#/definitions/generic_resources"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"restart_policy": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"condition": {"type": "string"},
|
||||
"delay": {"type": "string", "format": "duration"},
|
||||
"max_attempts": {"type": "integer"},
|
||||
"window": {"type": "string", "format": "duration"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"placement": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"constraints": {"type": "array", "items": {"type": "string"}},
|
||||
"preferences": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"spread": {"type": "string"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
|
||||
"generic_resources": {
|
||||
"id": "#/definitions/generic_resources",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"discrete_resource_spec": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {"type": "string"},
|
||||
"value": {"type": "number"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
|
||||
"network": {
|
||||
"id": "#/definitions/network",
|
||||
"type": ["object", "null"],
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"driver": {"type": "string"},
|
||||
"driver_opts": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^.+$": {"type": ["string", "number"]}
|
||||
}
|
||||
},
|
||||
"ipam": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"driver": {"type": "string"},
|
||||
"config": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"subnet": {"type": "string"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"external": {
|
||||
"type": ["boolean", "object"],
|
||||
"properties": {
|
||||
"name": {"type": "string"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"internal": {"type": "boolean"},
|
||||
"attachable": {"type": "boolean"},
|
||||
"labels": {"$ref": "#/definitions/list_or_dict"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
|
||||
"volume": {
|
||||
"id": "#/definitions/volume",
|
||||
"type": ["object", "null"],
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"driver": {"type": "string"},
|
||||
"driver_opts": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^.+$": {"type": ["string", "number"]}
|
||||
}
|
||||
},
|
||||
"external": {
|
||||
"type": ["boolean", "object"],
|
||||
"properties": {
|
||||
"name": {"type": "string"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"labels": {"$ref": "#/definitions/list_or_dict"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
|
||||
"secret": {
|
||||
"id": "#/definitions/secret",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"file": {"type": "string"},
|
||||
"external": {
|
||||
"type": ["boolean", "object"],
|
||||
"properties": {
|
||||
"name": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"labels": {"$ref": "#/definitions/list_or_dict"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
|
||||
"config": {
|
||||
"id": "#/definitions/config",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"file": {"type": "string"},
|
||||
"external": {
|
||||
"type": ["boolean", "object"],
|
||||
"properties": {
|
||||
"name": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"labels": {"$ref": "#/definitions/list_or_dict"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
|
||||
"string_or_list": {
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{"$ref": "#/definitions/list_of_strings"}
|
||||
]
|
||||
},
|
||||
|
||||
"list_of_strings": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"uniqueItems": true
|
||||
},
|
||||
|
||||
"list_or_dict": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
".+": {
|
||||
"type": ["string", "number", "null"]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{"type": "array", "items": {"type": "string"}, "uniqueItems": true}
|
||||
]
|
||||
},
|
||||
|
||||
"constraints": {
|
||||
"service": {
|
||||
"id": "#/definitions/constraints/service",
|
||||
"anyOf": [
|
||||
{"required": ["build"]},
|
||||
{"required": ["image"]}
|
||||
],
|
||||
"properties": {
|
||||
"build": {
|
||||
"required": ["context"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ var UnsupportedProperties = []string{
|
||||
"security_opt",
|
||||
"shm_size",
|
||||
"sysctls",
|
||||
"tmpfs",
|
||||
"ulimits",
|
||||
"userns_mode",
|
||||
}
|
||||
@@ -283,7 +284,6 @@ type ServiceVolumeConfig struct {
|
||||
Consistency string
|
||||
Bind *ServiceVolumeBind
|
||||
Volume *ServiceVolumeVolume
|
||||
Tmpfs *ServiceVolumeTmpfs
|
||||
}
|
||||
|
||||
// ServiceVolumeBind are options for a service volume of type bind
|
||||
@@ -296,11 +296,6 @@ type ServiceVolumeVolume struct {
|
||||
NoCopy bool `mapstructure:"nocopy"`
|
||||
}
|
||||
|
||||
// ServiceVolumeTmpfs are options for a service volume of type tmpfs
|
||||
type ServiceVolumeTmpfs struct {
|
||||
Size int64
|
||||
}
|
||||
|
||||
// FileReferenceConfig for a reference to a swarm file object
|
||||
type FileReferenceConfig struct {
|
||||
Source string
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/manifest/types"
|
||||
"github.com/docker/distribution/reference"
|
||||
)
|
||||
|
||||
// Store manages local storage of image distribution manifests
|
||||
type Store interface {
|
||||
Remove(listRef reference.Reference) error
|
||||
Get(listRef reference.Reference, manifest reference.Reference) (types.ImageManifest, error)
|
||||
GetList(listRef reference.Reference) ([]types.ImageManifest, error)
|
||||
Save(listRef reference.Reference, manifest reference.Reference, image types.ImageManifest) error
|
||||
}
|
||||
|
||||
// fsStore manages manifest files stored on the local filesystem
|
||||
type fsStore struct {
|
||||
root string
|
||||
}
|
||||
|
||||
// NewStore returns a new store for a local file path
|
||||
func NewStore(root string) Store {
|
||||
return &fsStore{root: root}
|
||||
}
|
||||
|
||||
// Remove a manifest list from local storage
|
||||
func (s *fsStore) Remove(listRef reference.Reference) error {
|
||||
path := filepath.Join(s.root, makeFilesafeName(listRef.String()))
|
||||
return os.RemoveAll(path)
|
||||
}
|
||||
|
||||
// Get returns the local manifest
|
||||
func (s *fsStore) Get(listRef reference.Reference, manifest reference.Reference) (types.ImageManifest, error) {
|
||||
filename := manifestToFilename(s.root, listRef.String(), manifest.String())
|
||||
return s.getFromFilename(manifest, filename)
|
||||
}
|
||||
|
||||
func (s *fsStore) getFromFilename(ref reference.Reference, filename string) (types.ImageManifest, error) {
|
||||
bytes, err := ioutil.ReadFile(filename)
|
||||
switch {
|
||||
case os.IsNotExist(err):
|
||||
return types.ImageManifest{}, newNotFoundError(ref.String())
|
||||
case err != nil:
|
||||
return types.ImageManifest{}, err
|
||||
}
|
||||
var manifestInfo types.ImageManifest
|
||||
return manifestInfo, json.Unmarshal(bytes, &manifestInfo)
|
||||
}
|
||||
|
||||
// GetList returns all the local manifests for a transaction
|
||||
func (s *fsStore) GetList(listRef reference.Reference) ([]types.ImageManifest, error) {
|
||||
filenames, err := s.listManifests(listRef.String())
|
||||
switch {
|
||||
case err != nil:
|
||||
return nil, err
|
||||
case filenames == nil:
|
||||
return nil, newNotFoundError(listRef.String())
|
||||
}
|
||||
|
||||
manifests := []types.ImageManifest{}
|
||||
for _, filename := range filenames {
|
||||
filename = filepath.Join(s.root, makeFilesafeName(listRef.String()), filename)
|
||||
manifest, err := s.getFromFilename(listRef, filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
manifests = append(manifests, manifest)
|
||||
}
|
||||
return manifests, nil
|
||||
}
|
||||
|
||||
// listManifests stored in a transaction
|
||||
func (s *fsStore) listManifests(transaction string) ([]string, error) {
|
||||
transactionDir := filepath.Join(s.root, makeFilesafeName(transaction))
|
||||
fileInfos, err := ioutil.ReadDir(transactionDir)
|
||||
switch {
|
||||
case os.IsNotExist(err):
|
||||
return nil, nil
|
||||
case err != nil:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filenames := []string{}
|
||||
for _, info := range fileInfos {
|
||||
filenames = append(filenames, info.Name())
|
||||
}
|
||||
return filenames, nil
|
||||
}
|
||||
|
||||
// Save a manifest as part of a local manifest list
|
||||
func (s *fsStore) Save(listRef reference.Reference, manifest reference.Reference, image types.ImageManifest) error {
|
||||
if err := s.createManifestListDirectory(listRef.String()); err != nil {
|
||||
return err
|
||||
}
|
||||
filename := manifestToFilename(s.root, listRef.String(), manifest.String())
|
||||
bytes, err := json.Marshal(image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(filename, bytes, 0644)
|
||||
}
|
||||
|
||||
func (s *fsStore) createManifestListDirectory(transaction string) error {
|
||||
path := filepath.Join(s.root, makeFilesafeName(transaction))
|
||||
return os.MkdirAll(path, 0755)
|
||||
}
|
||||
|
||||
func manifestToFilename(root, manifestList, manifest string) string {
|
||||
return filepath.Join(root, makeFilesafeName(manifestList), makeFilesafeName(manifest))
|
||||
}
|
||||
|
||||
func makeFilesafeName(ref string) string {
|
||||
fileName := strings.Replace(ref, ":", "-", -1)
|
||||
return strings.Replace(fileName, "/", "_", -1)
|
||||
}
|
||||
|
||||
type notFoundError struct {
|
||||
object string
|
||||
}
|
||||
|
||||
func newNotFoundError(ref string) *notFoundError {
|
||||
return ¬FoundError{object: ref}
|
||||
}
|
||||
|
||||
func (n *notFoundError) Error() string {
|
||||
return fmt.Sprintf("No such manifest: %s", n.object)
|
||||
}
|
||||
|
||||
// NotFound interface
|
||||
func (n *notFoundError) NotFound() {}
|
||||
|
||||
// IsNotFound returns true if the error is a not found error
|
||||
func IsNotFound(err error) bool {
|
||||
_, ok := err.(notFound)
|
||||
return ok
|
||||
}
|
||||
|
||||
type notFound interface {
|
||||
NotFound()
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/manifest/types"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type fakeRef struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (f fakeRef) String() string {
|
||||
return f.name
|
||||
}
|
||||
|
||||
func (f fakeRef) Name() string {
|
||||
return f.name
|
||||
}
|
||||
|
||||
func ref(name string) fakeRef {
|
||||
return fakeRef{name: name}
|
||||
}
|
||||
|
||||
func sref(t *testing.T, name string) *types.SerializableNamed {
|
||||
named, err := reference.ParseNamed("example.com/" + name)
|
||||
require.NoError(t, err)
|
||||
return &types.SerializableNamed{Named: named}
|
||||
}
|
||||
|
||||
func newTestStore(t *testing.T) (Store, func()) {
|
||||
tmpdir, err := ioutil.TempDir("", "manifest-store-test")
|
||||
require.NoError(t, err)
|
||||
|
||||
return NewStore(tmpdir), func() { os.RemoveAll(tmpdir) }
|
||||
}
|
||||
|
||||
func getFiles(t *testing.T, store Store) []os.FileInfo {
|
||||
infos, err := ioutil.ReadDir(store.(*fsStore).root)
|
||||
require.NoError(t, err)
|
||||
return infos
|
||||
}
|
||||
|
||||
func TestStoreRemove(t *testing.T) {
|
||||
store, cleanup := newTestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
listRef := ref("list")
|
||||
data := types.ImageManifest{Ref: sref(t, "abcdef")}
|
||||
require.NoError(t, store.Save(listRef, ref("manifest"), data))
|
||||
require.Len(t, getFiles(t, store), 1)
|
||||
|
||||
assert.NoError(t, store.Remove(listRef))
|
||||
assert.Len(t, getFiles(t, store), 0)
|
||||
}
|
||||
|
||||
func TestStoreSaveAndGet(t *testing.T) {
|
||||
store, cleanup := newTestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
listRef := ref("list")
|
||||
data := types.ImageManifest{Ref: sref(t, "abcdef")}
|
||||
err := store.Save(listRef, ref("exists"), data)
|
||||
require.NoError(t, err)
|
||||
|
||||
var testcases = []struct {
|
||||
listRef reference.Reference
|
||||
manifestRef reference.Reference
|
||||
expected types.ImageManifest
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
listRef: listRef,
|
||||
manifestRef: ref("exists"),
|
||||
expected: data,
|
||||
},
|
||||
{
|
||||
listRef: listRef,
|
||||
manifestRef: ref("exist:does-not"),
|
||||
expectedErr: "No such manifest: exist:does-not",
|
||||
},
|
||||
{
|
||||
listRef: ref("list:does-not-exist"),
|
||||
manifestRef: ref("manifest:does-not-exist"),
|
||||
expectedErr: "No such manifest: manifest:does-not-exist",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
actual, err := store.Get(testcase.listRef, testcase.manifestRef)
|
||||
if testcase.expectedErr != "" {
|
||||
assert.EqualError(t, err, testcase.expectedErr)
|
||||
assert.True(t, IsNotFound(err))
|
||||
continue
|
||||
}
|
||||
if !assert.NoError(t, err, testcase.manifestRef.String()) {
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, testcase.expected, actual, testcase.manifestRef.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreGetList(t *testing.T) {
|
||||
store, cleanup := newTestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
listRef := ref("list")
|
||||
first := types.ImageManifest{Ref: sref(t, "first")}
|
||||
require.NoError(t, store.Save(listRef, ref("first"), first))
|
||||
second := types.ImageManifest{Ref: sref(t, "second")}
|
||||
require.NoError(t, store.Save(listRef, ref("exists"), second))
|
||||
|
||||
list, err := store.GetList(listRef)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, list, 2)
|
||||
}
|
||||
|
||||
func TestStoreGetListDoesNotExist(t *testing.T) {
|
||||
store, cleanup := newTestStore(t)
|
||||
defer cleanup()
|
||||
|
||||
listRef := ref("list")
|
||||
_, err := store.GetList(listRef)
|
||||
assert.EqualError(t, err, "No such manifest: list")
|
||||
assert.True(t, IsNotFound(err))
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/manifest/manifestlist"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// ImageManifest contains info to output for a manifest object.
|
||||
type ImageManifest struct {
|
||||
Ref *SerializableNamed
|
||||
Digest digest.Digest
|
||||
SchemaV2Manifest *schema2.DeserializedManifest `json:",omitempty"`
|
||||
Platform manifestlist.PlatformSpec
|
||||
}
|
||||
|
||||
// Blobs returns the digests for all the blobs referenced by this manifest
|
||||
func (i ImageManifest) Blobs() []digest.Digest {
|
||||
digests := []digest.Digest{}
|
||||
for _, descriptor := range i.SchemaV2Manifest.References() {
|
||||
digests = append(digests, descriptor.Digest)
|
||||
}
|
||||
return digests
|
||||
}
|
||||
|
||||
// Payload returns the media type and bytes for the manifest
|
||||
func (i ImageManifest) Payload() (string, []byte, error) {
|
||||
switch {
|
||||
case i.SchemaV2Manifest != nil:
|
||||
return i.SchemaV2Manifest.Payload()
|
||||
default:
|
||||
return "", nil, errors.Errorf("%s has no payload", i.Ref)
|
||||
}
|
||||
}
|
||||
|
||||
// References implements the distribution.Manifest interface. It delegates to
|
||||
// the underlying manifest.
|
||||
func (i ImageManifest) References() []distribution.Descriptor {
|
||||
switch {
|
||||
case i.SchemaV2Manifest != nil:
|
||||
return i.SchemaV2Manifest.References()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// NewImageManifest returns a new ImageManifest object. The values for Platform
|
||||
// are initialized from those in the image
|
||||
func NewImageManifest(ref reference.Named, digest digest.Digest, img Image, manifest *schema2.DeserializedManifest) ImageManifest {
|
||||
platform := manifestlist.PlatformSpec{
|
||||
OS: img.OS,
|
||||
Architecture: img.Architecture,
|
||||
OSVersion: img.OSVersion,
|
||||
OSFeatures: img.OSFeatures,
|
||||
}
|
||||
return ImageManifest{
|
||||
Ref: &SerializableNamed{Named: ref},
|
||||
Digest: digest,
|
||||
SchemaV2Manifest: manifest,
|
||||
Platform: platform,
|
||||
}
|
||||
}
|
||||
|
||||
// SerializableNamed is a reference.Named that can be serialzied and deserialized
|
||||
// from JSON
|
||||
type SerializableNamed struct {
|
||||
reference.Named
|
||||
}
|
||||
|
||||
// UnmarshalJSON loads the Named reference from JSON bytes
|
||||
func (s *SerializableNamed) UnmarshalJSON(b []byte) error {
|
||||
var raw string
|
||||
if err := json.Unmarshal(b, &raw); err != nil {
|
||||
return errors.Wrapf(err, "invalid named reference bytes: %s", b)
|
||||
}
|
||||
var err error
|
||||
s.Named, err = reference.ParseNamed(raw)
|
||||
return err
|
||||
}
|
||||
|
||||
// MarshalJSON returns the JSON bytes representation
|
||||
func (s *SerializableNamed) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(s.String())
|
||||
}
|
||||
|
||||
// Image is the minimal set of fields required to set default platform settings
|
||||
// on a manifest.
|
||||
type Image struct {
|
||||
Architecture string `json:"architecture,omitempty"`
|
||||
OS string `json:"os,omitempty"`
|
||||
OSVersion string `json:"os.version,omitempty"`
|
||||
OSFeatures []string `json:"os.features,omitempty"`
|
||||
}
|
||||
|
||||
// NewImageFromJSON creates an Image configuration from json.
|
||||
func NewImageFromJSON(src []byte) (*Image, error) {
|
||||
img := &Image{}
|
||||
if err := json.Unmarshal(src, img); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return img, nil
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
manifesttypes "github.com/docker/cli/cli/manifest/types"
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/reference"
|
||||
distributionclient "github.com/docker/distribution/registry/client"
|
||||
"github.com/docker/docker/api/types"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// RegistryClient is a client used to communicate with a Docker distribution
|
||||
// registry
|
||||
type RegistryClient interface {
|
||||
GetManifest(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error)
|
||||
GetManifestList(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error)
|
||||
MountBlob(ctx context.Context, source reference.Canonical, target reference.Named) error
|
||||
PutManifest(ctx context.Context, ref reference.Named, manifest distribution.Manifest) (digest.Digest, error)
|
||||
}
|
||||
|
||||
// NewRegistryClient returns a new RegistryClient with a resolver
|
||||
func NewRegistryClient(resolver AuthConfigResolver, userAgent string, insecure bool) RegistryClient {
|
||||
return &client{
|
||||
authConfigResolver: resolver,
|
||||
insecureRegistry: insecure,
|
||||
userAgent: userAgent,
|
||||
}
|
||||
}
|
||||
|
||||
// AuthConfigResolver returns Auth Configuration for an index
|
||||
type AuthConfigResolver func(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig
|
||||
|
||||
// PutManifestOptions is the data sent to push a manifest
|
||||
type PutManifestOptions struct {
|
||||
MediaType string
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
type client struct {
|
||||
authConfigResolver AuthConfigResolver
|
||||
insecureRegistry bool
|
||||
userAgent string
|
||||
}
|
||||
|
||||
// ErrBlobCreated returned when a blob mount request was created
|
||||
type ErrBlobCreated struct {
|
||||
From reference.Named
|
||||
Target reference.Named
|
||||
}
|
||||
|
||||
func (err ErrBlobCreated) Error() string {
|
||||
return fmt.Sprintf("blob mounted from: %v to: %v",
|
||||
err.From, err.Target)
|
||||
}
|
||||
|
||||
// ErrHTTPProto returned if attempting to use TLS with a non-TLS registry
|
||||
type ErrHTTPProto struct {
|
||||
OrigErr string
|
||||
}
|
||||
|
||||
func (err ErrHTTPProto) Error() string {
|
||||
return err.OrigErr
|
||||
}
|
||||
|
||||
var _ RegistryClient = &client{}
|
||||
|
||||
// MountBlob into the registry, so it can be referenced by a manifest
|
||||
func (c *client) MountBlob(ctx context.Context, sourceRef reference.Canonical, targetRef reference.Named) error {
|
||||
repoEndpoint, err := newDefaultRepositoryEndpoint(targetRef, c.insecureRegistry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
repo, err := c.getRepositoryForReference(ctx, targetRef, repoEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lu, err := repo.Blobs(ctx).Create(ctx, distributionclient.WithMountFrom(sourceRef))
|
||||
switch err.(type) {
|
||||
case distribution.ErrBlobMounted:
|
||||
logrus.Debugf("mount of blob %s succeeded", sourceRef)
|
||||
return nil
|
||||
case nil:
|
||||
default:
|
||||
return errors.Wrapf(err, "failed to mount blob %s to %s", sourceRef, targetRef)
|
||||
}
|
||||
lu.Cancel(ctx)
|
||||
logrus.Debugf("mount of blob %s created", sourceRef)
|
||||
return ErrBlobCreated{From: sourceRef, Target: targetRef}
|
||||
}
|
||||
|
||||
// PutManifest sends the manifest to a registry and returns the new digest
|
||||
func (c *client) PutManifest(ctx context.Context, ref reference.Named, manifest distribution.Manifest) (digest.Digest, error) {
|
||||
repoEndpoint, err := newDefaultRepositoryEndpoint(ref, c.insecureRegistry)
|
||||
if err != nil {
|
||||
return digest.Digest(""), err
|
||||
}
|
||||
|
||||
repo, err := c.getRepositoryForReference(ctx, ref, repoEndpoint)
|
||||
if err != nil {
|
||||
return digest.Digest(""), err
|
||||
}
|
||||
|
||||
manifestService, err := repo.Manifests(ctx)
|
||||
if err != nil {
|
||||
return digest.Digest(""), err
|
||||
}
|
||||
|
||||
_, opts, err := getManifestOptionsFromReference(ref)
|
||||
if err != nil {
|
||||
return digest.Digest(""), err
|
||||
}
|
||||
|
||||
dgst, err := manifestService.Put(ctx, manifest, opts...)
|
||||
return dgst, errors.Wrapf(err, "failed to put manifest %s", ref)
|
||||
}
|
||||
|
||||
func (c *client) getRepositoryForReference(ctx context.Context, ref reference.Named, repoEndpoint repositoryEndpoint) (distribution.Repository, error) {
|
||||
httpTransport, err := c.getHTTPTransportForRepoEndpoint(ctx, repoEndpoint)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "server gave HTTP response to HTTPS client") {
|
||||
return nil, ErrHTTPProto{OrigErr: err.Error()}
|
||||
}
|
||||
}
|
||||
repoName, err := reference.WithName(repoEndpoint.Name())
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to parse repo name from %s", ref)
|
||||
}
|
||||
return distributionclient.NewRepository(ctx, repoName, repoEndpoint.BaseURL(), httpTransport)
|
||||
}
|
||||
|
||||
func (c *client) getHTTPTransportForRepoEndpoint(ctx context.Context, repoEndpoint repositoryEndpoint) (http.RoundTripper, error) {
|
||||
httpTransport, err := getHTTPTransport(
|
||||
c.authConfigResolver(ctx, repoEndpoint.info.Index),
|
||||
repoEndpoint.endpoint,
|
||||
repoEndpoint.Name(),
|
||||
c.userAgent)
|
||||
return httpTransport, errors.Wrap(err, "failed to configure transport")
|
||||
}
|
||||
|
||||
// GetManifest returns an ImageManifest for the reference
|
||||
func (c *client) GetManifest(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) {
|
||||
var result manifesttypes.ImageManifest
|
||||
fetch := func(ctx context.Context, repo distribution.Repository, ref reference.Named) (bool, error) {
|
||||
var err error
|
||||
result, err = fetchManifest(ctx, repo, ref)
|
||||
return result.Ref != nil, err
|
||||
}
|
||||
|
||||
err := c.iterateEndpoints(ctx, ref, fetch)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// GetManifestList returns a list of ImageManifest for the reference
|
||||
func (c *client) GetManifestList(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error) {
|
||||
result := []manifesttypes.ImageManifest{}
|
||||
fetch := func(ctx context.Context, repo distribution.Repository, ref reference.Named) (bool, error) {
|
||||
var err error
|
||||
result, err = fetchList(ctx, repo, ref)
|
||||
return len(result) > 0, err
|
||||
}
|
||||
|
||||
err := c.iterateEndpoints(ctx, ref, fetch)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func getManifestOptionsFromReference(ref reference.Named) (digest.Digest, []distribution.ManifestServiceOption, error) {
|
||||
if tagged, isTagged := ref.(reference.NamedTagged); isTagged {
|
||||
tag := tagged.Tag()
|
||||
return "", []distribution.ManifestServiceOption{distribution.WithTag(tag)}, nil
|
||||
}
|
||||
if digested, isDigested := ref.(reference.Canonical); isDigested {
|
||||
return digested.Digest(), []distribution.ManifestServiceOption{}, nil
|
||||
}
|
||||
return "", nil, errors.Errorf("%s no tag or digest", ref)
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/distribution/registry/client/auth"
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
authtypes "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type repositoryEndpoint struct {
|
||||
info *registry.RepositoryInfo
|
||||
endpoint registry.APIEndpoint
|
||||
}
|
||||
|
||||
// Name returns the repository name
|
||||
func (r repositoryEndpoint) Name() string {
|
||||
repoName := r.info.Name.Name()
|
||||
// If endpoint does not support CanonicalName, use the RemoteName instead
|
||||
if r.endpoint.TrimHostname {
|
||||
repoName = reference.Path(r.info.Name)
|
||||
}
|
||||
return repoName
|
||||
}
|
||||
|
||||
// BaseURL returns the endpoint url
|
||||
func (r repositoryEndpoint) BaseURL() string {
|
||||
return r.endpoint.URL.String()
|
||||
}
|
||||
|
||||
func newDefaultRepositoryEndpoint(ref reference.Named, insecure bool) (repositoryEndpoint, error) {
|
||||
repoInfo, err := registry.ParseRepositoryInfo(ref)
|
||||
if err != nil {
|
||||
return repositoryEndpoint{}, err
|
||||
}
|
||||
endpoint, err := getDefaultEndpointFromRepoInfo(repoInfo)
|
||||
if err != nil {
|
||||
return repositoryEndpoint{}, err
|
||||
}
|
||||
if insecure {
|
||||
endpoint.TLSConfig.InsecureSkipVerify = true
|
||||
}
|
||||
return repositoryEndpoint{info: repoInfo, endpoint: endpoint}, nil
|
||||
}
|
||||
|
||||
func getDefaultEndpointFromRepoInfo(repoInfo *registry.RepositoryInfo) (registry.APIEndpoint, error) {
|
||||
var err error
|
||||
|
||||
options := registry.ServiceOptions{}
|
||||
registryService, err := registry.NewService(options)
|
||||
if err != nil {
|
||||
return registry.APIEndpoint{}, err
|
||||
}
|
||||
endpoints, err := registryService.LookupPushEndpoints(reference.Domain(repoInfo.Name))
|
||||
if err != nil {
|
||||
return registry.APIEndpoint{}, err
|
||||
}
|
||||
// Default to the highest priority endpoint to return
|
||||
endpoint := endpoints[0]
|
||||
if !repoInfo.Index.Secure {
|
||||
for _, ep := range endpoints {
|
||||
if ep.URL.Scheme == "http" {
|
||||
endpoint = ep
|
||||
}
|
||||
}
|
||||
}
|
||||
return endpoint, nil
|
||||
}
|
||||
|
||||
// getHTTPTransport builds a transport for use in communicating with a registry
|
||||
func getHTTPTransport(authConfig authtypes.AuthConfig, endpoint registry.APIEndpoint, repoName string, userAgent string) (http.RoundTripper, error) {
|
||||
// get the http transport, this will be used in a client to upload manifest
|
||||
base := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
DualStack: true,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
TLSClientConfig: endpoint.TLSConfig,
|
||||
DisableKeepAlives: true,
|
||||
}
|
||||
|
||||
modifiers := registry.Headers(userAgent, http.Header{})
|
||||
authTransport := transport.NewTransport(base, modifiers...)
|
||||
challengeManager, confirmedV2, err := registry.PingV2Registry(endpoint.URL, authTransport)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error pinging v2 registry")
|
||||
}
|
||||
if !confirmedV2 {
|
||||
return nil, fmt.Errorf("unsupported registry version")
|
||||
}
|
||||
if authConfig.RegistryToken != "" {
|
||||
passThruTokenHandler := &existingTokenHandler{token: authConfig.RegistryToken}
|
||||
modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, passThruTokenHandler))
|
||||
} else {
|
||||
creds := registry.NewStaticCredentialStore(&authConfig)
|
||||
tokenHandler := auth.NewTokenHandler(authTransport, creds, repoName, "*")
|
||||
basicHandler := auth.NewBasicHandler(creds)
|
||||
modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
|
||||
}
|
||||
return transport.NewTransport(base, modifiers...), nil
|
||||
}
|
||||
|
||||
// RepoNameForReference returns the repository name from a reference
|
||||
func RepoNameForReference(ref reference.Named) (string, error) {
|
||||
// insecure is fine since this only returns the name
|
||||
repo, err := newDefaultRepositoryEndpoint(ref, false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return repo.Name(), nil
|
||||
}
|
||||
|
||||
type existingTokenHandler struct {
|
||||
token string
|
||||
}
|
||||
|
||||
func (th *existingTokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", th.token))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (th *existingTokenHandler) Scheme() string {
|
||||
return "bearer"
|
||||
}
|
||||
@@ -1,295 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli/manifest/types"
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/manifest/manifestlist"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/distribution/registry/api/errcode"
|
||||
"github.com/docker/distribution/registry/api/v2"
|
||||
distclient "github.com/docker/distribution/registry/client"
|
||||
"github.com/docker/docker/registry"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// fetchManifest pulls a manifest from a registry and returns it. An error
|
||||
// is returned if no manifest is found matching namedRef.
|
||||
func fetchManifest(ctx context.Context, repo distribution.Repository, ref reference.Named) (types.ImageManifest, error) {
|
||||
manifest, err := getManifest(ctx, repo, ref)
|
||||
if err != nil {
|
||||
return types.ImageManifest{}, err
|
||||
}
|
||||
|
||||
switch v := manifest.(type) {
|
||||
// Removed Schema 1 support
|
||||
case *schema2.DeserializedManifest:
|
||||
imageManifest, err := pullManifestSchemaV2(ctx, ref, repo, *v)
|
||||
if err != nil {
|
||||
return types.ImageManifest{}, err
|
||||
}
|
||||
return imageManifest, nil
|
||||
case *manifestlist.DeserializedManifestList:
|
||||
return types.ImageManifest{}, errors.Errorf("%s is a manifest list", ref)
|
||||
}
|
||||
return types.ImageManifest{}, errors.Errorf("%s is not a manifest", ref)
|
||||
}
|
||||
|
||||
func fetchList(ctx context.Context, repo distribution.Repository, ref reference.Named) ([]types.ImageManifest, error) {
|
||||
manifest, err := getManifest(ctx, repo, ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch v := manifest.(type) {
|
||||
case *manifestlist.DeserializedManifestList:
|
||||
imageManifests, err := pullManifestList(ctx, ref, repo, *v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return imageManifests, nil
|
||||
default:
|
||||
return nil, errors.Errorf("unsupported manifest format: %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func getManifest(ctx context.Context, repo distribution.Repository, ref reference.Named) (distribution.Manifest, error) {
|
||||
manSvc, err := repo.Manifests(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dgst, opts, err := getManifestOptionsFromReference(ref)
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("image manifest for %q does not exist", ref)
|
||||
}
|
||||
return manSvc.Get(ctx, dgst, opts...)
|
||||
}
|
||||
|
||||
func pullManifestSchemaV2(ctx context.Context, ref reference.Named, repo distribution.Repository, mfst schema2.DeserializedManifest) (types.ImageManifest, error) {
|
||||
manifestDigest, err := validateManifestDigest(ref, mfst)
|
||||
if err != nil {
|
||||
return types.ImageManifest{}, err
|
||||
}
|
||||
configJSON, err := pullManifestSchemaV2ImageConfig(ctx, mfst.Target().Digest, repo)
|
||||
if err != nil {
|
||||
return types.ImageManifest{}, err
|
||||
}
|
||||
|
||||
img, err := types.NewImageFromJSON(configJSON)
|
||||
if err != nil {
|
||||
return types.ImageManifest{}, err
|
||||
}
|
||||
return types.NewImageManifest(ref, manifestDigest, *img, &mfst), nil
|
||||
}
|
||||
|
||||
func pullManifestSchemaV2ImageConfig(ctx context.Context, dgst digest.Digest, repo distribution.Repository) ([]byte, error) {
|
||||
blobs := repo.Blobs(ctx)
|
||||
configJSON, err := blobs.Get(ctx, dgst)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
verifier := dgst.Verifier()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := verifier.Write(configJSON); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !verifier.Verified() {
|
||||
return nil, errors.Errorf("image config verification failed for digest %s", dgst)
|
||||
}
|
||||
return configJSON, nil
|
||||
}
|
||||
|
||||
// validateManifestDigest computes the manifest digest, and, if pulling by
|
||||
// digest, ensures that it matches the requested digest.
|
||||
func validateManifestDigest(ref reference.Named, mfst distribution.Manifest) (digest.Digest, error) {
|
||||
_, canonical, err := mfst.Payload()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// If pull by digest, then verify the manifest digest.
|
||||
if digested, isDigested := ref.(reference.Canonical); isDigested {
|
||||
verifier := digested.Digest().Verifier()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := verifier.Write(canonical); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !verifier.Verified() {
|
||||
err := fmt.Errorf("manifest verification failed for digest %s", digested.Digest())
|
||||
return "", err
|
||||
}
|
||||
return digested.Digest(), nil
|
||||
}
|
||||
|
||||
return digest.FromBytes(canonical), nil
|
||||
}
|
||||
|
||||
// pullManifestList handles "manifest lists" which point to various
|
||||
// platform-specific manifests.
|
||||
func pullManifestList(ctx context.Context, ref reference.Named, repo distribution.Repository, mfstList manifestlist.DeserializedManifestList) ([]types.ImageManifest, error) {
|
||||
infos := []types.ImageManifest{}
|
||||
|
||||
if _, err := validateManifestDigest(ref, mfstList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, manifestDescriptor := range mfstList.Manifests {
|
||||
manSvc, err := repo.Manifests(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
manifest, err := manSvc.Get(ctx, manifestDescriptor.Digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v, ok := manifest.(*schema2.DeserializedManifest)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported manifest format: %s", v)
|
||||
}
|
||||
|
||||
manifestRef, err := reference.WithDigest(ref, manifestDescriptor.Digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
imageManifest, err := pullManifestSchemaV2(ctx, manifestRef, repo, *v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
imageManifest.Platform = manifestDescriptor.Platform
|
||||
infos = append(infos, imageManifest)
|
||||
}
|
||||
return infos, nil
|
||||
}
|
||||
|
||||
func continueOnError(err error) bool {
|
||||
switch v := err.(type) {
|
||||
case errcode.Errors:
|
||||
if len(v) == 0 {
|
||||
return true
|
||||
}
|
||||
return continueOnError(v[0])
|
||||
case errcode.Error:
|
||||
e := err.(errcode.Error)
|
||||
switch e.Code {
|
||||
case errcode.ErrorCodeUnauthorized, v2.ErrorCodeManifestUnknown, v2.ErrorCodeNameUnknown:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case *distclient.UnexpectedHTTPResponseError:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *client) iterateEndpoints(ctx context.Context, namedRef reference.Named, each func(context.Context, distribution.Repository, reference.Named) (bool, error)) error {
|
||||
endpoints, err := allEndpoints(namedRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repoInfo, err := registry.ParseRepositoryInfo(namedRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
confirmedTLSRegistries := make(map[string]bool)
|
||||
for _, endpoint := range endpoints {
|
||||
|
||||
if endpoint.Version == registry.APIVersion1 {
|
||||
logrus.Debugf("skipping v1 endpoint %s", endpoint.URL)
|
||||
continue
|
||||
}
|
||||
|
||||
if endpoint.URL.Scheme != "https" {
|
||||
if _, confirmedTLS := confirmedTLSRegistries[endpoint.URL.Host]; confirmedTLS {
|
||||
logrus.Debugf("skipping non-TLS endpoint %s for host/port that appears to use TLS", endpoint.URL)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if c.insecureRegistry {
|
||||
endpoint.TLSConfig.InsecureSkipVerify = true
|
||||
}
|
||||
repoEndpoint := repositoryEndpoint{endpoint: endpoint, info: repoInfo}
|
||||
repo, err := c.getRepositoryForReference(ctx, namedRef, repoEndpoint)
|
||||
if err != nil {
|
||||
logrus.Debugf("error with repo endpoint %s: %s", repoEndpoint, err)
|
||||
if _, ok := err.(ErrHTTPProto); ok {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if endpoint.URL.Scheme == "http" && !c.insecureRegistry {
|
||||
logrus.Debugf("skipping non-tls registry endpoint: %s", endpoint.URL)
|
||||
continue
|
||||
}
|
||||
done, err := each(ctx, repo, namedRef)
|
||||
if err != nil {
|
||||
if continueOnError(err) {
|
||||
if endpoint.URL.Scheme == "https" {
|
||||
confirmedTLSRegistries[endpoint.URL.Host] = true
|
||||
}
|
||||
logrus.Debugf("continuing on error (%T) %s", err, err)
|
||||
continue
|
||||
}
|
||||
logrus.Debugf("not continuing on error (%T) %s", err, err)
|
||||
return err
|
||||
}
|
||||
if done {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return newNotFoundError(namedRef.String())
|
||||
}
|
||||
|
||||
// allEndpoints returns a list of endpoints ordered by priority (v2, https, v1).
|
||||
func allEndpoints(namedRef reference.Named) ([]registry.APIEndpoint, error) {
|
||||
repoInfo, err := registry.ParseRepositoryInfo(namedRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
registryService, err := registry.NewService(registry.ServiceOptions{})
|
||||
if err != nil {
|
||||
return []registry.APIEndpoint{}, err
|
||||
}
|
||||
endpoints, err := registryService.LookupPullEndpoints(reference.Domain(repoInfo.Name))
|
||||
logrus.Debugf("endpoints for %s: %v", namedRef, endpoints)
|
||||
return endpoints, err
|
||||
}
|
||||
|
||||
type notFoundError struct {
|
||||
object string
|
||||
}
|
||||
|
||||
func newNotFoundError(ref string) *notFoundError {
|
||||
return ¬FoundError{object: ref}
|
||||
}
|
||||
|
||||
func (n *notFoundError) Error() string {
|
||||
return fmt.Sprintf("no such manifest: %s", n.object)
|
||||
}
|
||||
|
||||
// NotFound interface
|
||||
func (n *notFoundError) NotFound() {}
|
||||
|
||||
// IsNotFound returns true if the error is a not found error
|
||||
func IsNotFound(err error) bool {
|
||||
_, ok := err.(notFound)
|
||||
return ok
|
||||
}
|
||||
|
||||
type notFound interface {
|
||||
NotFound()
|
||||
}
|
||||
@@ -249,7 +249,12 @@ func isSupported(cmd *cobra.Command, details versionDetails) error {
|
||||
if err := areSubcommandsSupported(cmd, details); err != nil {
|
||||
return err
|
||||
}
|
||||
return areFlagsSupported(cmd, details)
|
||||
|
||||
if err := areFlagsSupported(cmd, details); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func areFlagsSupported(cmd *cobra.Command, details versionDetails) error {
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#
|
||||
|
||||
DEV_DOCKER_IMAGE_NAME = docker-cli-dev$(IMAGE_TAG)
|
||||
BINARY_NATIVE_IMAGE_NAME = docker-cli-native$(IMAGE_TAG)
|
||||
LINTER_IMAGE_NAME = docker-cli-lint$(IMAGE_TAG)
|
||||
CROSS_IMAGE_NAME = docker-cli-cross$(IMAGE_TAG)
|
||||
VALIDATE_IMAGE_NAME = docker-cli-shell-validate$(IMAGE_TAG)
|
||||
@@ -31,18 +30,12 @@ build_cross_image:
|
||||
build_shell_validate_image:
|
||||
docker build -t $(VALIDATE_IMAGE_NAME) -f ./dockerfiles/Dockerfile.shellcheck .
|
||||
|
||||
.PHONY: build_binary_native_image
|
||||
build_binary_native_image:
|
||||
docker build -t $(BINARY_NATIVE_IMAGE_NAME) -f ./dockerfiles/Dockerfile.binary-native .
|
||||
|
||||
|
||||
# build executable using a container
|
||||
binary: build_binary_native_image
|
||||
docker run --rm $(ENVVARS) $(MOUNTS) $(BINARY_NATIVE_IMAGE_NAME)
|
||||
binary: build_docker_image
|
||||
docker run --rm $(ENVVARS) $(MOUNTS) $(DEV_DOCKER_IMAGE_NAME) make binary
|
||||
|
||||
build: binary
|
||||
|
||||
|
||||
# clean build artifacts using a container
|
||||
.PHONY: clean
|
||||
clean: build_docker_image
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
FROM golang:1.9.2-alpine3.6
|
||||
|
||||
RUN apk add -U git bash coreutils gcc musl-dev
|
||||
|
||||
ENV CGO_ENABLED=0 \
|
||||
DISABLE_WARN_OUTSIDE_CONTAINER=1
|
||||
WORKDIR /go/src/github.com/docker/cli
|
||||
CMD ./scripts/build/binary
|
||||
@@ -767,30 +767,6 @@ publish the port when running the container, use the `-p` flag on `docker run`
|
||||
to publish and map one or more ports, or the `-P` flag to publish all exposed
|
||||
ports and map them to high-order ports.
|
||||
|
||||
By default, `EXPOSE` assumes TCP. You can also specify UDP:
|
||||
|
||||
```Dockerfile
|
||||
EXPOSE 80/udp
|
||||
```
|
||||
|
||||
To expose on both TCP and UDP, include two lines:
|
||||
|
||||
```Dockerfile
|
||||
EXPOSE 80/tcp
|
||||
EXPOSE 80/udp
|
||||
```
|
||||
|
||||
In this case, if you use `-P` with `docker run`, the port will be exposed once
|
||||
for TCP and once for UDP. Remember that `-P` uses an ephemeral high-ordered host
|
||||
port on the host, so the port will not be the same for TCP and UDP.
|
||||
|
||||
Regardless of the `EXPOSE` settings, you can override them at runtime by using
|
||||
the `-p` flag. For example
|
||||
|
||||
```bash
|
||||
docker run -p 80:80/tcp -p 80:80/udp ...
|
||||
```
|
||||
|
||||
To set up port redirection on the host system, see [using the -P
|
||||
flag](run.md#expose-incoming-ports). The `docker network` command supports
|
||||
creating networks for communication among containers without the need to
|
||||
@@ -805,15 +781,14 @@ see the
|
||||
ENV <key>=<value> ...
|
||||
|
||||
The `ENV` instruction sets the environment variable `<key>` to the value
|
||||
`<value>`. This value will be in the environment for all subsequent instructions
|
||||
in the build stage and can be [replaced inline](#environment-replacement) in
|
||||
`<value>`. This value will be in the environment of all "descendant"
|
||||
`Dockerfile` commands and can be [replaced inline](#environment-replacement) in
|
||||
many as well.
|
||||
|
||||
The `ENV` instruction has two forms. The first form, `ENV <key> <value>`,
|
||||
will set a single variable to a value. The entire string after the first
|
||||
space will be treated as the `<value>` - including whitespace characters. The
|
||||
value will be interpreted for other environment variables, so quote characters
|
||||
will be removed if they are not escaped.
|
||||
space will be treated as the `<value>` - including characters such as
|
||||
spaces and quotes.
|
||||
|
||||
The second form, `ENV <key>=<value> ...`, allows for multiple variables to
|
||||
be set at one time. Notice that the second form uses the equals sign (=)
|
||||
@@ -831,7 +806,8 @@ and
|
||||
ENV myDog Rex The Dog
|
||||
ENV myCat fluffy
|
||||
|
||||
will yield the same net results in the final image.
|
||||
will yield the same net results in the final image, but the first form
|
||||
is preferred because it produces a single cache layer.
|
||||
|
||||
The environment variables set using `ENV` will persist when a container is run
|
||||
from the resulting image. You can view the values using `docker inspect`, and
|
||||
@@ -855,7 +831,7 @@ whitespace)
|
||||
> The `--chown` feature is only supported on Dockerfiles used to build Linux containers,
|
||||
> and will not work on Windows containers. Since user and group ownership concepts do
|
||||
> not translate between Linux and Windows, the use of `/etc/passwd` and `/etc/group` for
|
||||
> translating user and group names to IDs restricts this feature to only be viable
|
||||
> translating user and group names to IDs restricts this feature to only be viable for
|
||||
> for Linux OS-based containers.
|
||||
|
||||
The `ADD` instruction copies new files, directories or remote file URLs from `<src>`
|
||||
@@ -1845,7 +1821,7 @@ all previous `SHELL` instructions, and affects all subsequent instructions. For
|
||||
RUN Write-Host hello
|
||||
|
||||
# Executed as cmd /S /C echo hello
|
||||
SHELL ["cmd", "/S", "/C"]
|
||||
SHELL ["cmd", "/S"", "/C"]
|
||||
RUN echo hello
|
||||
|
||||
The following instructions can be affected by the `SHELL` instruction when the
|
||||
|
||||
@@ -18,8 +18,7 @@ keywords: "stdin, tarred, repository"
|
||||
```markdown
|
||||
Usage: docker load [OPTIONS]
|
||||
|
||||
Load an image or repository from a tar archive (even if compressed with gzip,
|
||||
bzip2, or xz) from a file or STDIN.
|
||||
Load an image from a tar archive or STDIN
|
||||
|
||||
Options:
|
||||
--help Print usage
|
||||
@@ -29,13 +28,13 @@ Options:
|
||||
```
|
||||
## Description
|
||||
|
||||
Load an image or repository from a tar archive (even if compressed with gzip,
|
||||
bzip2, or xz) from a file or STDIN. It restores both images and tags.
|
||||
`docker load` loads a tarred repository from a file or the standard input stream.
|
||||
It restores both images and tags.
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
$ docker docker image ls
|
||||
$ docker images
|
||||
|
||||
REPOSITORY TAG IMAGE ID CREATED SIZE
|
||||
|
||||
|
||||
@@ -1,274 +0,0 @@
|
||||
---
|
||||
title: "manifest"
|
||||
description: "The manifest command description and usage"
|
||||
keywords: "docker, manifest"
|
||||
---
|
||||
|
||||
<!-- This file is maintained within the docker/cli Github
|
||||
repository at https://github.com/docker/cli/. Make all
|
||||
pull requests against that repo. If you see this file in
|
||||
another repository, consider it read-only there, as it will
|
||||
periodically be overwritten by the definitive file. Pull
|
||||
requests which include edits to this file in other repositories
|
||||
will be rejected.
|
||||
-->
|
||||
|
||||
```markdown
|
||||
Usage: docker manifest COMMAND
|
||||
|
||||
Manage Docker image manifests and manifest lists
|
||||
|
||||
Options:
|
||||
--help Print usage
|
||||
|
||||
Commands:
|
||||
annotate Add additional information to a local image manifest
|
||||
create Create a local manifest list for annotating and pushing to a registry
|
||||
inspect Display an image manifest, or manifest list
|
||||
push Push a manifest list to a repository
|
||||
|
||||
```
|
||||
|
||||
## Description
|
||||
|
||||
The `docker manifest` command by itself performs no action. In order to operate
|
||||
on a manifest or manifest list, one of the subcommands must be used.
|
||||
|
||||
A single manifest is information about an image, such as layers, size, and digest.
|
||||
The docker manifest command also gives users additional information such as the os
|
||||
and architecture an image was built for.
|
||||
|
||||
A manifest list is a list of image layers that is created by specifying one or
|
||||
more (ideally more than one) image names. It can then be used in the same way as
|
||||
an image name in `docker pull` and `docker run` commands, for example.
|
||||
|
||||
Ideally a manifest list is created from images that are identical in function for
|
||||
different os/arch combinations. For this reason, manifest lists are often referred to as
|
||||
"multi-arch images." However, a user could create a manifest list that points
|
||||
to two images -- one for windows on amd64, and one for darwin on amd64.
|
||||
|
||||
### manifest inspect
|
||||
|
||||
```
|
||||
manifest inspect --help
|
||||
|
||||
Usage: docker manifest inspect [OPTIONS] [MANIFEST_LIST] MANIFEST
|
||||
|
||||
isplay an image manifest, or manifest list
|
||||
|
||||
Options:
|
||||
--help Print usage
|
||||
--insecure allow communication with an insecure registry
|
||||
-v, --verbose Output additional info including layers and platform
|
||||
```
|
||||
|
||||
### manifest create
|
||||
|
||||
```bash
|
||||
Usage: docker manifest create MANFEST_LIST MANIFEST [MANIFEST...]
|
||||
|
||||
Create a local manifest list for annotating and pushing to a registry
|
||||
|
||||
Options:
|
||||
-a, --amend Amend an existing manifest list
|
||||
--insecure allow communication with an insecure registry
|
||||
--help Print usage
|
||||
```
|
||||
|
||||
### manifest annotate
|
||||
```bash
|
||||
Usage: docker manifest annotate [OPTIONS] MANIFEST_LIST MANIFEST
|
||||
|
||||
Add additional information to a local image manifest
|
||||
|
||||
Options:
|
||||
--arch string Set architecture
|
||||
--help Print usage
|
||||
--os string Set operating system
|
||||
--os-features stringSlice Set operating system feature
|
||||
--variant string Set architecture variant
|
||||
|
||||
```
|
||||
|
||||
### manifest push
|
||||
```bash
|
||||
Usage: docker manifest push [OPTIONS] MANIFEST_LIST
|
||||
|
||||
Push a manifest list to a repository
|
||||
|
||||
Options:
|
||||
--help Print usage
|
||||
--insecure allow push to an insecure registry
|
||||
-p, --purge Remove the local manifest list after push
|
||||
```
|
||||
|
||||
### Working with insecure registries
|
||||
|
||||
The manifest command interacts solely with a Docker registry. Because of this, it has no way to query the engine for the list of allowed insecure registries. To allow the CLI to interact with an insecure registry, some `docker manifest` commands have an `--insecure` flag. For each transaction, such as a `create`, which queries a registry, the `--insecure` flag must be specified. This flag tells the CLI that this registry call may ignore security concerns like missing or self-signed certificates. Likewise, on a `manifest push` to an insecure registry, the `--insecure` flag must be specified. If this is not used with an insecure registry, the manifest command fails to find a registry that meets the default requirements.
|
||||
|
||||
## Examples
|
||||
|
||||
### inspect an image's manifest object
|
||||
|
||||
```bash
|
||||
$ docker manifest inspect hello-world
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"config": {
|
||||
"mediaType": "application/vnd.docker.container.image.v1+json",
|
||||
"size": 1520,
|
||||
"digest": "sha256:1815c82652c03bfd8644afda26fb184f2ed891d921b20a0703b46768f9755c57"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 972,
|
||||
"digest": "sha256:b04784fba78d739b526e27edc02a5a8cd07b1052e9283f5fc155828f4b614c28"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Inspect an image's manifest and get the os/arch info
|
||||
|
||||
The `docker manifest inspect` command takes an optional `--verbose` flag
|
||||
that gives you the image's name (Ref), and architecture and os (Platform).
|
||||
|
||||
Just as with other docker commands that take image names, you can refer to an image with or
|
||||
without a tag, or by digest (e.g. hello-world@sha256:f3b3b28a45160805bb16542c9531888519430e9e6d6ffc09d72261b0d26ff74f).
|
||||
|
||||
Here is an example of inspecting an image's manifest with the `--verbose` flag:
|
||||
|
||||
```bash
|
||||
$ docker manifest inspect -v hello-world
|
||||
{
|
||||
"Ref": "docker.io/library/hello-world:latest",
|
||||
"Digest": "sha256:f3b3b28a45160805bb16542c9531888519430e9e6d6ffc09d72261b0d26ff74f",
|
||||
"SchemaV2Manifest": {
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"config": {
|
||||
"mediaType": "application/vnd.docker.container.image.v1+json",
|
||||
"size": 1520,
|
||||
"digest": "sha256:1815c82652c03bfd8644afda26fb184f2ed891d921b20a0703b46768f9755c57"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 972,
|
||||
"digest": "sha256:b04784fba78d739b526e27edc02a5a8cd07b1052e9283f5fc155828f4b614c28"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Platform": {
|
||||
"architecture": "amd64",
|
||||
"os": "linux"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Create and push a manifest list
|
||||
|
||||
To create a manifest list, you first `create` the manifest list locally by specifying the constituent images you would
|
||||
like to have included in your manifest list. Keep in mind that this is pushed to a registry, so if you want to push
|
||||
to a registry other than the docker registry, you need to create your manifest list with the registry name or IP and port.
|
||||
This is similar to tagging an image and pushing it to a foreign registry.
|
||||
|
||||
After you have created your local copy of the manifest list, you may optionally
|
||||
`annotate` it. Annotations allowed are the architecture and operating system (overriding the image's current values),
|
||||
os features, and an archictecure variant.
|
||||
|
||||
Finally, you need to `push` your manifest list to the desired registry. Below are descriptions of these three commands,
|
||||
and an example putting them all together.
|
||||
|
||||
```bash
|
||||
$ docker manifest create 45.55.81.106:5000/coolapp:v1 \
|
||||
45.55.81.106:5000/coolapp-ppc64le-linux:v1 \
|
||||
45.55.81.106:5000/coolapp-arm-linux:v1 \
|
||||
45.55.81.106:5000/coolapp-amd64-linux:v1 \
|
||||
45.55.81.106:5000/coolapp-amd64-windows:v1
|
||||
Created manifest list 45.55.81.106:5000/coolapp:v1
|
||||
```
|
||||
|
||||
```bash
|
||||
$ docker manifest annotate 45.55.81.106:5000/coolapp:v1 45.55.81.106:5000/coolapp-arm-linux --arch arm
|
||||
```
|
||||
|
||||
```bash
|
||||
$ docker manifest push 45.55.81.106:5000/coolapp:v1
|
||||
Pushed manifest 45.55.81.106:5000/coolapp@sha256:9701edc932223a66e49dd6c894a11db8c2cf4eccd1414f1ec105a623bf16b426 with digest: sha256:f67dcc5fc786f04f0743abfe0ee5dae9bd8caf8efa6c8144f7f2a43889dc513b
|
||||
Pushed manifest 45.55.81.106:5000/coolapp@sha256:f3b3b28a45160805bb16542c9531888519430e9e6d6ffc09d72261b0d26ff74f with digest: sha256:b64ca0b60356a30971f098c92200b1271257f100a55b351e6bbe985638352f3a
|
||||
Pushed manifest 45.55.81.106:5000/coolapp@sha256:39dc41c658cf25f33681a41310372f02728925a54aac3598310bfb1770615fc9 with digest: sha256:df436846483aff62bad830b730a0d3b77731bcf98ba5e470a8bbb8e9e346e4e8
|
||||
Pushed manifest 45.55.81.106:5000/coolapp@sha256:f91b1145cd4ac800b28122313ae9e88ac340bb3f1e3a4cd3e59a3648650f3275 with digest: sha256:5bb8e50aa2edd408bdf3ddf61efb7338ff34a07b762992c9432f1c02fc0e5e62
|
||||
sha256:050b213d49d7673ba35014f21454c573dcbec75254a08f4a7c34f66a47c06aba
|
||||
|
||||
```
|
||||
|
||||
### Inspect a manifest list
|
||||
|
||||
```bash
|
||||
$ docker manifest inspect coolapp:v1
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
|
||||
"manifests": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"size": 424,
|
||||
"digest": "sha256:f67dcc5fc786f04f0743abfe0ee5dae9bd8caf8efa6c8144f7f2a43889dc513b",
|
||||
"platform": {
|
||||
"architecture": "arm",
|
||||
"os": "linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"size": 424,
|
||||
"digest": "sha256:b64ca0b60356a30971f098c92200b1271257f100a55b351e6bbe985638352f3a",
|
||||
"platform": {
|
||||
"architecture": "amd64",
|
||||
"os": "linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"size": 425,
|
||||
"digest": "sha256:df436846483aff62bad830b730a0d3b77731bcf98ba5e470a8bbb8e9e346e4e8",
|
||||
"platform": {
|
||||
"architecture": "ppc64le",
|
||||
"os": "linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"size": 425,
|
||||
"digest": "sha256:5bb8e50aa2edd408bdf3ddf61efb7338ff34a07b762992c9432f1c02fc0e5e62",
|
||||
"platform": {
|
||||
"architecture": "s390x",
|
||||
"os": "linux"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Push to an insecure registry
|
||||
|
||||
Here is an example of creating and pushing a manifest list using a known insecure registry.
|
||||
|
||||
```
|
||||
$ docker manifest create --insecure myprivateregistry.mycompany.com/repo/image:1.0 \
|
||||
myprivateregistry.mycompany.com/repo/image-linux-ppc64le:1.0 \
|
||||
myprivateregistry.mycompany.com/repo/image-linux-s390x:1.0 \
|
||||
myprivateregistry.mycompany.com/repo/image-linux-arm:1.0 \
|
||||
myprivateregistry.mycompany.com/repo/image-linux-armhf:1.0 \
|
||||
myprivateregistry.mycompany.com/repo/image-windows-amd64:1.0 \
|
||||
myprivateregistry.mycompany.com/repo/image-linux-amd64:1.0
|
||||
```
|
||||
```
|
||||
$ docker manifest push --insecure myprivateregistry.mycompany.com/repo/image:tag
|
||||
```
|
||||
|
||||
Note that the `--insecure` flag is not required to annotate a manifest list, since annotations are to a locally-stored copy of a manifest list. You may also skip the `--insecure` flag if you are performaing a `docker manifest inspect` on a locally-stored manifest list. Be sure to keep in mind that locally-stored manifest lists are never used by the engine on a `docker pull`.
|
||||
|
||||
@@ -764,7 +764,6 @@ The options you can specify are:
|
||||
<th>Long syntax</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr>
|
||||
<td>published and target port </td>
|
||||
<td><tt></tt></td>
|
||||
|
||||
@@ -79,7 +79,10 @@ func GenYamlTreeCustom(cmd *cobra.Command, dir string, filePrepender func(string
|
||||
if _, err := io.WriteString(f, filePrepender(filename)); err != nil {
|
||||
return err
|
||||
}
|
||||
return GenYamlCustom(cmd, f)
|
||||
if err := GenYamlCustom(cmd, f); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenYamlCustom creates custom yaml output
|
||||
@@ -120,7 +123,7 @@ func GenYamlCustom(cmd *cobra.Command, w io.Writer) error {
|
||||
cliDoc.Kubernetes = true
|
||||
}
|
||||
if _, ok := curr.Annotations["swarm"]; ok && !cliDoc.Swarm {
|
||||
cliDoc.Swarm = true
|
||||
cliDoc.Kubernetes = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,7 +208,7 @@ func genFlagResult(flags *pflag.FlagSet) []cmdOption {
|
||||
opt.Kubernetes = true
|
||||
}
|
||||
if _, ok := flag.Annotations["swarm"]; ok {
|
||||
opt.Swarm = true
|
||||
opt.Kubernetes = true
|
||||
}
|
||||
|
||||
result = append(result, opt)
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/e2e/internal/fixtures"
|
||||
"github.com/gotestyourself/gotestyourself/icmd"
|
||||
)
|
||||
|
||||
func TestAttachExitCode(t *testing.T) {
|
||||
containerID := runBackgroundContainsWithExitCode(t, 21)
|
||||
|
||||
result := icmd.RunCmd(
|
||||
icmd.Command("docker", "attach", containerID),
|
||||
withStdinNewline)
|
||||
|
||||
result.Assert(t, icmd.Expected{ExitCode: 21})
|
||||
}
|
||||
|
||||
func runBackgroundContainsWithExitCode(t *testing.T, exitcode int) string {
|
||||
result := icmd.RunCmd(shell(t,
|
||||
"docker run -d -i --rm %s sh -c 'read; exit %d'", fixtures.AlpineImage, exitcode))
|
||||
result.Assert(t, icmd.Success)
|
||||
return strings.TrimSpace(result.Stdout())
|
||||
}
|
||||
|
||||
func withStdinNewline(cmd *icmd.Cmd) {
|
||||
cmd.Stdin = strings.NewReader("\n")
|
||||
}
|
||||
@@ -9,8 +9,6 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
manifeststore "github.com/docker/cli/cli/manifest/store"
|
||||
registryclient "github.com/docker/cli/cli/registry/client"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/docker/client"
|
||||
notaryclient "github.com/theupdateframework/notary/client"
|
||||
@@ -31,8 +29,6 @@ type FakeCli struct {
|
||||
server command.ServerInfo
|
||||
clientInfoFunc clientInfoFuncType
|
||||
notaryClientFunc notaryClientFuncType
|
||||
manifestStore manifeststore.Store
|
||||
registryClient registryclient.RegistryClient
|
||||
}
|
||||
|
||||
// NewFakeCli returns a fake for the command.Cli interface
|
||||
@@ -129,23 +125,3 @@ func (c *FakeCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []st
|
||||
}
|
||||
return nil, fmt.Errorf("no notary client available unless defined")
|
||||
}
|
||||
|
||||
// ManifestStore returns a fake store used for testing
|
||||
func (c *FakeCli) ManifestStore() manifeststore.Store {
|
||||
return c.manifestStore
|
||||
}
|
||||
|
||||
// RegistryClient returns a fake client for testing
|
||||
func (c *FakeCli) RegistryClient(insecure bool) registryclient.RegistryClient {
|
||||
return c.registryClient
|
||||
}
|
||||
|
||||
// SetManifestStore on the fake cli
|
||||
func (c *FakeCli) SetManifestStore(store manifeststore.Store) {
|
||||
c.manifestStore = store
|
||||
}
|
||||
|
||||
// SetRegistryClient on the fake cli
|
||||
func (c *FakeCli) SetRegistryClient(client registryclient.RegistryClient) {
|
||||
c.registryClient = client
|
||||
}
|
||||
|
||||
@@ -5,10 +5,9 @@
|
||||
package v1beta1
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
conversion "k8s.io/apimachinery/pkg/conversion"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// Deprecated: register deep-copy functions.
|
||||
|
||||
@@ -5,10 +5,9 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
conversion "k8s.io/apimachinery/pkg/conversion"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// Deprecated: register deep-copy functions.
|
||||
@@ -104,13 +103,12 @@ func (x *Owner) DeepCopy() *Owner {
|
||||
}
|
||||
|
||||
func (x *Owner) DeepCopyObject() runtime.Object {
|
||||
if c := x.DeepCopy(); c != nil {
|
||||
return c
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
if c := x.DeepCopy(); c != nil {
|
||||
return c
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *OwnerList) DeepCopyInto(out *OwnerList) {
|
||||
*out = *in
|
||||
|
||||
-1
@@ -1 +0,0 @@
|
||||
package manifest
|
||||
Generated
Vendored
-155
@@ -1,155 +0,0 @@
|
||||
package manifestlist
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/manifest"
|
||||
"github.com/opencontainers/go-digest"
|
||||
)
|
||||
|
||||
// MediaTypeManifestList specifies the mediaType for manifest lists.
|
||||
const MediaTypeManifestList = "application/vnd.docker.distribution.manifest.list.v2+json"
|
||||
|
||||
// SchemaVersion provides a pre-initialized version structure for this
|
||||
// packages version of the manifest.
|
||||
var SchemaVersion = manifest.Versioned{
|
||||
SchemaVersion: 2,
|
||||
MediaType: MediaTypeManifestList,
|
||||
}
|
||||
|
||||
func init() {
|
||||
manifestListFunc := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) {
|
||||
m := new(DeserializedManifestList)
|
||||
err := m.UnmarshalJSON(b)
|
||||
if err != nil {
|
||||
return nil, distribution.Descriptor{}, err
|
||||
}
|
||||
|
||||
dgst := digest.FromBytes(b)
|
||||
return m, distribution.Descriptor{Digest: dgst, Size: int64(len(b)), MediaType: MediaTypeManifestList}, err
|
||||
}
|
||||
err := distribution.RegisterManifestSchema(MediaTypeManifestList, manifestListFunc)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Unable to register manifest: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
// PlatformSpec specifies a platform where a particular image manifest is
|
||||
// applicable.
|
||||
type PlatformSpec struct {
|
||||
// Architecture field specifies the CPU architecture, for example
|
||||
// `amd64` or `ppc64`.
|
||||
Architecture string `json:"architecture"`
|
||||
|
||||
// OS specifies the operating system, for example `linux` or `windows`.
|
||||
OS string `json:"os"`
|
||||
|
||||
// OSVersion is an optional field specifying the operating system
|
||||
// version, for example `10.0.10586`.
|
||||
OSVersion string `json:"os.version,omitempty"`
|
||||
|
||||
// OSFeatures is an optional field specifying an array of strings,
|
||||
// each listing a required OS feature (for example on Windows `win32k`).
|
||||
OSFeatures []string `json:"os.features,omitempty"`
|
||||
|
||||
// Variant is an optional field specifying a variant of the CPU, for
|
||||
// example `ppc64le` to specify a little-endian version of a PowerPC CPU.
|
||||
Variant string `json:"variant,omitempty"`
|
||||
|
||||
// Features is an optional field specifying an array of strings, each
|
||||
// listing a required CPU feature (for example `sse4` or `aes`).
|
||||
Features []string `json:"features,omitempty"`
|
||||
}
|
||||
|
||||
// A ManifestDescriptor references a platform-specific manifest.
|
||||
type ManifestDescriptor struct {
|
||||
distribution.Descriptor
|
||||
|
||||
// Platform specifies which platform the manifest pointed to by the
|
||||
// descriptor runs on.
|
||||
Platform PlatformSpec `json:"platform"`
|
||||
}
|
||||
|
||||
// ManifestList references manifests for various platforms.
|
||||
type ManifestList struct {
|
||||
manifest.Versioned
|
||||
|
||||
// Config references the image configuration as a blob.
|
||||
Manifests []ManifestDescriptor `json:"manifests"`
|
||||
}
|
||||
|
||||
// References returns the distribution descriptors for the referenced image
|
||||
// manifests.
|
||||
func (m ManifestList) References() []distribution.Descriptor {
|
||||
dependencies := make([]distribution.Descriptor, len(m.Manifests))
|
||||
for i := range m.Manifests {
|
||||
dependencies[i] = m.Manifests[i].Descriptor
|
||||
}
|
||||
|
||||
return dependencies
|
||||
}
|
||||
|
||||
// DeserializedManifestList wraps ManifestList with a copy of the original
|
||||
// JSON.
|
||||
type DeserializedManifestList struct {
|
||||
ManifestList
|
||||
|
||||
// canonical is the canonical byte representation of the Manifest.
|
||||
canonical []byte
|
||||
}
|
||||
|
||||
// FromDescriptors takes a slice of descriptors, and returns a
|
||||
// DeserializedManifestList which contains the resulting manifest list
|
||||
// and its JSON representation.
|
||||
func FromDescriptors(descriptors []ManifestDescriptor) (*DeserializedManifestList, error) {
|
||||
m := ManifestList{
|
||||
Versioned: SchemaVersion,
|
||||
}
|
||||
|
||||
m.Manifests = make([]ManifestDescriptor, len(descriptors), len(descriptors))
|
||||
copy(m.Manifests, descriptors)
|
||||
|
||||
deserialized := DeserializedManifestList{
|
||||
ManifestList: m,
|
||||
}
|
||||
|
||||
var err error
|
||||
deserialized.canonical, err = json.MarshalIndent(&m, "", " ")
|
||||
return &deserialized, err
|
||||
}
|
||||
|
||||
// UnmarshalJSON populates a new ManifestList struct from JSON data.
|
||||
func (m *DeserializedManifestList) UnmarshalJSON(b []byte) error {
|
||||
m.canonical = make([]byte, len(b), len(b))
|
||||
// store manifest list in canonical
|
||||
copy(m.canonical, b)
|
||||
|
||||
// Unmarshal canonical JSON into ManifestList object
|
||||
var manifestList ManifestList
|
||||
if err := json.Unmarshal(m.canonical, &manifestList); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.ManifestList = manifestList
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON returns the contents of canonical. If canonical is empty,
|
||||
// marshals the inner contents.
|
||||
func (m *DeserializedManifestList) MarshalJSON() ([]byte, error) {
|
||||
if len(m.canonical) > 0 {
|
||||
return m.canonical, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("JSON representation not initialized in DeserializedManifestList")
|
||||
}
|
||||
|
||||
// Payload returns the raw content of the manifest list. The contents can be
|
||||
// used to calculate the content identifier.
|
||||
func (m DeserializedManifestList) Payload() (string, []byte, error) {
|
||||
return m.MediaType, m.canonical, nil
|
||||
}
|
||||
-84
@@ -1,84 +0,0 @@
|
||||
package schema2
|
||||
|
||||
import (
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/context"
|
||||
"github.com/opencontainers/go-digest"
|
||||
)
|
||||
|
||||
// builder is a type for constructing manifests.
|
||||
type builder struct {
|
||||
// bs is a BlobService used to publish the configuration blob.
|
||||
bs distribution.BlobService
|
||||
|
||||
// configMediaType is media type used to describe configuration
|
||||
configMediaType string
|
||||
|
||||
// configJSON references
|
||||
configJSON []byte
|
||||
|
||||
// dependencies is a list of descriptors that gets built by successive
|
||||
// calls to AppendReference. In case of image configuration these are layers.
|
||||
dependencies []distribution.Descriptor
|
||||
}
|
||||
|
||||
// NewManifestBuilder is used to build new manifests for the current schema
|
||||
// version. It takes a BlobService so it can publish the configuration blob
|
||||
// as part of the Build process.
|
||||
func NewManifestBuilder(bs distribution.BlobService, configMediaType string, configJSON []byte) distribution.ManifestBuilder {
|
||||
mb := &builder{
|
||||
bs: bs,
|
||||
configMediaType: configMediaType,
|
||||
configJSON: make([]byte, len(configJSON)),
|
||||
}
|
||||
copy(mb.configJSON, configJSON)
|
||||
|
||||
return mb
|
||||
}
|
||||
|
||||
// Build produces a final manifest from the given references.
|
||||
func (mb *builder) Build(ctx context.Context) (distribution.Manifest, error) {
|
||||
m := Manifest{
|
||||
Versioned: SchemaVersion,
|
||||
Layers: make([]distribution.Descriptor, len(mb.dependencies)),
|
||||
}
|
||||
copy(m.Layers, mb.dependencies)
|
||||
|
||||
configDigest := digest.FromBytes(mb.configJSON)
|
||||
|
||||
var err error
|
||||
m.Config, err = mb.bs.Stat(ctx, configDigest)
|
||||
switch err {
|
||||
case nil:
|
||||
// Override MediaType, since Put always replaces the specified media
|
||||
// type with application/octet-stream in the descriptor it returns.
|
||||
m.Config.MediaType = mb.configMediaType
|
||||
return FromStruct(m)
|
||||
case distribution.ErrBlobUnknown:
|
||||
// nop
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add config to the blob store
|
||||
m.Config, err = mb.bs.Put(ctx, mb.configMediaType, mb.configJSON)
|
||||
// Override MediaType, since Put always replaces the specified media
|
||||
// type with application/octet-stream in the descriptor it returns.
|
||||
m.Config.MediaType = mb.configMediaType
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return FromStruct(m)
|
||||
}
|
||||
|
||||
// AppendReference adds a reference to the current ManifestBuilder.
|
||||
func (mb *builder) AppendReference(d distribution.Describable) error {
|
||||
mb.dependencies = append(mb.dependencies, d.Descriptor())
|
||||
return nil
|
||||
}
|
||||
|
||||
// References returns the current references added to this builder.
|
||||
func (mb *builder) References() []distribution.Descriptor {
|
||||
return mb.dependencies
|
||||
}
|
||||
Generated
Vendored
-138
@@ -1,138 +0,0 @@
|
||||
package schema2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/manifest"
|
||||
"github.com/opencontainers/go-digest"
|
||||
)
|
||||
|
||||
const (
|
||||
// MediaTypeManifest specifies the mediaType for the current version.
|
||||
MediaTypeManifest = "application/vnd.docker.distribution.manifest.v2+json"
|
||||
|
||||
// MediaTypeImageConfig specifies the mediaType for the image configuration.
|
||||
MediaTypeImageConfig = "application/vnd.docker.container.image.v1+json"
|
||||
|
||||
// MediaTypePluginConfig specifies the mediaType for plugin configuration.
|
||||
MediaTypePluginConfig = "application/vnd.docker.plugin.v1+json"
|
||||
|
||||
// MediaTypeLayer is the mediaType used for layers referenced by the
|
||||
// manifest.
|
||||
MediaTypeLayer = "application/vnd.docker.image.rootfs.diff.tar.gzip"
|
||||
|
||||
// MediaTypeForeignLayer is the mediaType used for layers that must be
|
||||
// downloaded from foreign URLs.
|
||||
MediaTypeForeignLayer = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip"
|
||||
|
||||
// MediaTypeUncompressedLayer is the mediaType used for layers which
|
||||
// are not compressed.
|
||||
MediaTypeUncompressedLayer = "application/vnd.docker.image.rootfs.diff.tar"
|
||||
)
|
||||
|
||||
var (
|
||||
// SchemaVersion provides a pre-initialized version structure for this
|
||||
// packages version of the manifest.
|
||||
SchemaVersion = manifest.Versioned{
|
||||
SchemaVersion: 2,
|
||||
MediaType: MediaTypeManifest,
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
schema2Func := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) {
|
||||
m := new(DeserializedManifest)
|
||||
err := m.UnmarshalJSON(b)
|
||||
if err != nil {
|
||||
return nil, distribution.Descriptor{}, err
|
||||
}
|
||||
|
||||
dgst := digest.FromBytes(b)
|
||||
return m, distribution.Descriptor{Digest: dgst, Size: int64(len(b)), MediaType: MediaTypeManifest}, err
|
||||
}
|
||||
err := distribution.RegisterManifestSchema(MediaTypeManifest, schema2Func)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Unable to register manifest: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
// Manifest defines a schema2 manifest.
|
||||
type Manifest struct {
|
||||
manifest.Versioned
|
||||
|
||||
// Config references the image configuration as a blob.
|
||||
Config distribution.Descriptor `json:"config"`
|
||||
|
||||
// Layers lists descriptors for the layers referenced by the
|
||||
// configuration.
|
||||
Layers []distribution.Descriptor `json:"layers"`
|
||||
}
|
||||
|
||||
// References returnes the descriptors of this manifests references.
|
||||
func (m Manifest) References() []distribution.Descriptor {
|
||||
references := make([]distribution.Descriptor, 0, 1+len(m.Layers))
|
||||
references = append(references, m.Config)
|
||||
references = append(references, m.Layers...)
|
||||
return references
|
||||
}
|
||||
|
||||
// Target returns the target of this signed manifest.
|
||||
func (m Manifest) Target() distribution.Descriptor {
|
||||
return m.Config
|
||||
}
|
||||
|
||||
// DeserializedManifest wraps Manifest with a copy of the original JSON.
|
||||
// It satisfies the distribution.Manifest interface.
|
||||
type DeserializedManifest struct {
|
||||
Manifest
|
||||
|
||||
// canonical is the canonical byte representation of the Manifest.
|
||||
canonical []byte
|
||||
}
|
||||
|
||||
// FromStruct takes a Manifest structure, marshals it to JSON, and returns a
|
||||
// DeserializedManifest which contains the manifest and its JSON representation.
|
||||
func FromStruct(m Manifest) (*DeserializedManifest, error) {
|
||||
var deserialized DeserializedManifest
|
||||
deserialized.Manifest = m
|
||||
|
||||
var err error
|
||||
deserialized.canonical, err = json.MarshalIndent(&m, "", " ")
|
||||
return &deserialized, err
|
||||
}
|
||||
|
||||
// UnmarshalJSON populates a new Manifest struct from JSON data.
|
||||
func (m *DeserializedManifest) UnmarshalJSON(b []byte) error {
|
||||
m.canonical = make([]byte, len(b), len(b))
|
||||
// store manifest in canonical
|
||||
copy(m.canonical, b)
|
||||
|
||||
// Unmarshal canonical JSON into Manifest object
|
||||
var manifest Manifest
|
||||
if err := json.Unmarshal(m.canonical, &manifest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.Manifest = manifest
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON returns the contents of canonical. If canonical is empty,
|
||||
// marshals the inner contents.
|
||||
func (m *DeserializedManifest) MarshalJSON() ([]byte, error) {
|
||||
if len(m.canonical) > 0 {
|
||||
return m.canonical, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("JSON representation not initialized in DeserializedManifest")
|
||||
}
|
||||
|
||||
// Payload returns the raw content of the manifest. The contents can be used to
|
||||
// calculate the content identifier.
|
||||
func (m DeserializedManifest) Payload() (string, []byte, error) {
|
||||
return m.MediaType, m.canonical, nil
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
package manifest
|
||||
|
||||
// Versioned provides a struct with the manifest schemaVersion and mediaType.
|
||||
// Incoming content with unknown schema version can be decoded against this
|
||||
// struct to check the version.
|
||||
type Versioned struct {
|
||||
// SchemaVersion is the image manifest schema that this image follows
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
|
||||
// MediaType is the media type of this schema.
|
||||
MediaType string `json:"mediaType,omitempty"`
|
||||
}
|
||||
@@ -62,7 +62,6 @@ RUN apt-get update && apt-get install -y \
|
||||
libudev-dev \
|
||||
mercurial \
|
||||
net-tools \
|
||||
pigz \
|
||||
pkg-config \
|
||||
protobuf-compiler \
|
||||
protobuf-c-compiler \
|
||||
@@ -88,7 +87,7 @@ RUN apt-get update && apt-get install -y \
|
||||
# will need updating, to avoid errors. Ping #docker-maintainers on IRC
|
||||
# with a heads-up.
|
||||
# IMPORTANT: When updating this please note that stdlib archive/tar pkg is vendored
|
||||
ENV GO_VERSION 1.9.3
|
||||
ENV GO_VERSION 1.9.2
|
||||
RUN curl -fsSL "https://golang.org/dl/go${GO_VERSION}.linux-amd64.tar.gz" \
|
||||
| tar -xzC /usr/local
|
||||
|
||||
@@ -168,12 +167,11 @@ RUN echo "source $PWD/hack/make/.integration-test-helpers" >> /etc/bash.bashrc
|
||||
|
||||
# Get useful and necessary Hub images so we can "docker load" locally instead of pulling
|
||||
COPY contrib/download-frozen-image-v2.sh /go/src/github.com/docker/docker/contrib/
|
||||
# TODO: when issue #35963 fixed, we can upgrade the busybox to multi-arch
|
||||
RUN ./contrib/download-frozen-image-v2.sh /docker-frozen-images \
|
||||
buildpack-deps:jessie@sha256:dd86dced7c9cd2a724e779730f0a53f93b7ef42228d4344b25ce9a42a1486251 \
|
||||
buildpack-deps:jessie@sha256:85b379ec16065e4fe4127eb1c5fb1bcc03c559bd36dbb2e22ff496de55925fa6 \
|
||||
busybox:latest@sha256:32f093055929dbc23dec4d03e09dfe971f5973a9ca5cf059cbfb644c206aa83f \
|
||||
debian:jessie@sha256:287a20c5f73087ab406e6b364833e3fb7b3ae63ca0eb3486555dc27ed32c6e60 \
|
||||
hello-world:latest@sha256:be0cd392e45be79ffeffa6b05338b98ebb16c87b255f48e297ec7f98e123905c
|
||||
debian:jessie@sha256:72f784399fd2719b4cb4e16ef8e369a39dc67f53d978cd3e2e7bf4e502c7b793 \
|
||||
hello-world:latest@sha256:c5515758d4c5e1e838e9cd307f6c6a0d620b5e07e6f927b07d05f6d12a1ac8d7
|
||||
# See also ensureFrozenImagesLinux() in "integration-cli/fixtures_linux_daemon_test.go" (which needs to be updated when adding images to this list)
|
||||
|
||||
# Install tomlv, vndr, runc, containerd, tini, docker-proxy dockercli
|
||||
@@ -191,7 +189,7 @@ RUN ln -s /usr/local/completion/bash/docker /etc/bash_completion.d/docker
|
||||
ENTRYPOINT ["hack/dind"]
|
||||
|
||||
# Options for hack/validate/gometalinter
|
||||
ENV GOMETALINTER_OPTS="--deadline=2m"
|
||||
ENV GOMETALINTER_OPTS="--deadline 2m"
|
||||
|
||||
# Upload docker source
|
||||
COPY . /go/src/github.com/docker/docker
|
||||
|
||||
@@ -52,7 +52,6 @@ RUN apt-get update && apt-get install -y \
|
||||
libudev-dev \
|
||||
mercurial \
|
||||
net-tools \
|
||||
pigz \
|
||||
pkg-config \
|
||||
protobuf-compiler \
|
||||
protobuf-c-compiler \
|
||||
@@ -74,7 +73,7 @@ RUN apt-get update && apt-get install -y \
|
||||
|
||||
# Install Go
|
||||
# IMPORTANT: When updating this please note that stdlib archive/tar pkg is vendored
|
||||
ENV GO_VERSION 1.9.3
|
||||
ENV GO_VERSION 1.9.2
|
||||
RUN curl -fsSL "https://golang.org/dl/go${GO_VERSION}.linux-arm64.tar.gz" \
|
||||
| tar -xzC /usr/local
|
||||
|
||||
@@ -160,7 +159,7 @@ ENV PATH=/usr/local/cli:$PATH
|
||||
ENTRYPOINT ["hack/dind"]
|
||||
|
||||
# Options for hack/validate/gometalinter
|
||||
ENV GOMETALINTER_OPTS="--deadline=4m -j2"
|
||||
ENV GOMETALINTER_OPTS="--deadline 4m -j2"
|
||||
|
||||
# Upload docker source
|
||||
COPY . /go/src/github.com/docker/docker
|
||||
|
||||
@@ -45,7 +45,6 @@ RUN apt-get update && apt-get install -y \
|
||||
libtool \
|
||||
libudev-dev \
|
||||
mercurial \
|
||||
pigz \
|
||||
pkg-config \
|
||||
python-backports.ssl-match-hostname \
|
||||
python-dev \
|
||||
@@ -64,7 +63,7 @@ RUN apt-get update && apt-get install -y \
|
||||
|
||||
# Install Go
|
||||
# IMPORTANT: When updating this please note that stdlib archive/tar pkg is vendored
|
||||
ENV GO_VERSION 1.9.3
|
||||
ENV GO_VERSION 1.9.2
|
||||
RUN curl -fsSL "https://golang.org/dl/go${GO_VERSION}.linux-armv6l.tar.gz" \
|
||||
| tar -xzC /usr/local
|
||||
ENV PATH /go/bin:/usr/local/go/bin:$PATH
|
||||
@@ -132,10 +131,10 @@ RUN ln -sv $PWD/contrib/completion/bash/docker /etc/bash_completion.d/docker
|
||||
# Get useful and necessary Hub images so we can "docker load" locally instead of pulling
|
||||
COPY contrib/download-frozen-image-v2.sh /go/src/github.com/docker/docker/contrib/
|
||||
RUN ./contrib/download-frozen-image-v2.sh /docker-frozen-images \
|
||||
buildpack-deps:jessie@sha256:dd86dced7c9cd2a724e779730f0a53f93b7ef42228d4344b25ce9a42a1486251 \
|
||||
busybox:latest@sha256:bbc3a03235220b170ba48a157dd097dd1379299370e1ed99ce976df0355d24f0 \
|
||||
debian:jessie@sha256:287a20c5f73087ab406e6b364833e3fb7b3ae63ca0eb3486555dc27ed32c6e60 \
|
||||
hello-world:latest@sha256:be0cd392e45be79ffeffa6b05338b98ebb16c87b255f48e297ec7f98e123905c
|
||||
armhf/buildpack-deps:jessie@sha256:eb2dad77ef53e88d94c3c83862d315c806ea1ca49b6e74f4db362381365ce489 \
|
||||
armhf/busybox:latest@sha256:016a1e149d2acc2a3789a160dfa60ce870794eea27ad5e96f7a101970e5e1689 \
|
||||
armhf/debian:jessie@sha256:ac59fa18b28d0ef751eabb5ba4c4b5a9063f99398bae2f70495aa8ed6139b577 \
|
||||
armhf/hello-world:latest@sha256:9701edc932223a66e49dd6c894a11db8c2cf4eccd1414f1ec105a623bf16b426
|
||||
# See also ensureFrozenImagesLinux() in "integration-cli/fixtures_linux_daemon_test.go" (which needs to be updated when adding images to this list)
|
||||
|
||||
# Install tomlv, vndr, runc, containerd, tini, docker-proxy
|
||||
@@ -148,7 +147,7 @@ ENV PATH=/usr/local/cli:$PATH
|
||||
ENTRYPOINT ["hack/dind"]
|
||||
|
||||
# Options for hack/validate/gometalinter
|
||||
ENV GOMETALINTER_OPTS="--deadline=10m -j2"
|
||||
ENV GOMETALINTER_OPTS="--deadline 10m -j2"
|
||||
|
||||
# Upload docker source
|
||||
COPY . /go/src/github.com/docker/docker
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
## Step 1: Build tests
|
||||
FROM golang:1.9.3-alpine3.6 as builder
|
||||
FROM golang:1.9.2-alpine3.6 as builder
|
||||
|
||||
RUN apk add --update \
|
||||
bash \
|
||||
@@ -16,10 +16,10 @@ WORKDIR /go/src/github.com/docker/docker/
|
||||
# Generate frozen images
|
||||
COPY contrib/download-frozen-image-v2.sh contrib/download-frozen-image-v2.sh
|
||||
RUN contrib/download-frozen-image-v2.sh /output/docker-frozen-images \
|
||||
buildpack-deps:jessie@sha256:dd86dced7c9cd2a724e779730f0a53f93b7ef42228d4344b25ce9a42a1486251 \
|
||||
busybox:latest@sha256:bbc3a03235220b170ba48a157dd097dd1379299370e1ed99ce976df0355d24f0 \
|
||||
debian:jessie@sha256:287a20c5f73087ab406e6b364833e3fb7b3ae63ca0eb3486555dc27ed32c6e60 \
|
||||
hello-world:latest@sha256:be0cd392e45be79ffeffa6b05338b98ebb16c87b255f48e297ec7f98e123905c
|
||||
buildpack-deps:jessie@sha256:85b379ec16065e4fe4127eb1c5fb1bcc03c559bd36dbb2e22ff496de55925fa6 \
|
||||
busybox:latest@sha256:32f093055929dbc23dec4d03e09dfe971f5973a9ca5cf059cbfb644c206aa83f \
|
||||
debian:jessie@sha256:72f784399fd2719b4cb4e16ef8e369a39dc67f53d978cd3e2e7bf4e502c7b793 \
|
||||
hello-world:latest@sha256:c5515758d4c5e1e838e9cd307f6c6a0d620b5e07e6f927b07d05f6d12a1ac8d7
|
||||
|
||||
# Download Docker CLI binary
|
||||
COPY hack/dockerfile hack/dockerfile
|
||||
@@ -47,7 +47,6 @@ RUN apk add --update \
|
||||
g++ \
|
||||
git \
|
||||
iptables \
|
||||
pigz \
|
||||
tar \
|
||||
xz \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
@@ -46,7 +46,6 @@ RUN apt-get update && apt-get install -y \
|
||||
libtool \
|
||||
libudev-dev \
|
||||
mercurial \
|
||||
pigz \
|
||||
pkg-config \
|
||||
python-backports.ssl-match-hostname \
|
||||
python-dev \
|
||||
@@ -65,7 +64,7 @@ RUN apt-get update && apt-get install -y \
|
||||
# Install Go
|
||||
# NOTE: official ppc64le go binaries weren't available until go 1.6.4 and 1.7.4
|
||||
# IMPORTANT: When updating this please note that stdlib archive/tar pkg is vendored
|
||||
ENV GO_VERSION 1.9.3
|
||||
ENV GO_VERSION 1.9.2
|
||||
RUN curl -fsSL "https://golang.org/dl/go${GO_VERSION}.linux-ppc64le.tar.gz" \
|
||||
| tar -xzC /usr/local
|
||||
|
||||
@@ -130,10 +129,10 @@ RUN ln -sv $PWD/contrib/completion/bash/docker /etc/bash_completion.d/docker
|
||||
# Get useful and necessary Hub images so we can "docker load" locally instead of pulling
|
||||
COPY contrib/download-frozen-image-v2.sh /go/src/github.com/docker/docker/contrib/
|
||||
RUN ./contrib/download-frozen-image-v2.sh /docker-frozen-images \
|
||||
buildpack-deps:jessie@sha256:dd86dced7c9cd2a724e779730f0a53f93b7ef42228d4344b25ce9a42a1486251 \
|
||||
busybox:latest@sha256:bbc3a03235220b170ba48a157dd097dd1379299370e1ed99ce976df0355d24f0 \
|
||||
debian:jessie@sha256:287a20c5f73087ab406e6b364833e3fb7b3ae63ca0eb3486555dc27ed32c6e60 \
|
||||
hello-world:latest@sha256:be0cd392e45be79ffeffa6b05338b98ebb16c87b255f48e297ec7f98e123905c
|
||||
ppc64le/buildpack-deps:jessie@sha256:1a2f2d2cc8738f14b336aeffc3503b5c9dedf9e1f26c7313cb4999534ad4716f \
|
||||
ppc64le/busybox:latest@sha256:54f34c83adfab20cf0e630d879e210f07b0062cd6caaf16346a61396d50e7584 \
|
||||
ppc64le/debian:jessie@sha256:ea8c5b105e3790f075145b40e4be1e4488c9f33f55e6cc45182047b80a68f892 \
|
||||
ppc64le/hello-world:latest@sha256:7d57adf137665f748956c86089320710b66d08584db3500ed98f4bb3da637c2d
|
||||
# See also ensureFrozenImagesLinux() in "integration-cli/fixtures_linux_daemon_test.go" (which needs to be updated when adding images to this list)
|
||||
|
||||
# Install tomlv, vndr, runc, containerd, tini, docker-proxy
|
||||
|
||||
@@ -42,7 +42,6 @@ RUN apt-get update && apt-get install -y \
|
||||
libtool \
|
||||
libudev-dev \
|
||||
mercurial \
|
||||
pigz \
|
||||
pkg-config \
|
||||
python-backports.ssl-match-hostname \
|
||||
python-dev \
|
||||
@@ -59,7 +58,7 @@ RUN apt-get update && apt-get install -y \
|
||||
--no-install-recommends
|
||||
|
||||
# IMPORTANT: When updating this please note that stdlib archive/tar pkg is vendored
|
||||
ENV GO_VERSION 1.9.3
|
||||
ENV GO_VERSION 1.9.2
|
||||
RUN curl -fsSL "https://golang.org/dl/go${GO_VERSION}.linux-s390x.tar.gz" \
|
||||
| tar -xzC /usr/local
|
||||
|
||||
@@ -124,10 +123,10 @@ RUN ln -sv $PWD/contrib/completion/bash/docker /etc/bash_completion.d/docker
|
||||
# Get useful and necessary Hub images so we can "docker load" locally instead of pulling
|
||||
COPY contrib/download-frozen-image-v2.sh /go/src/github.com/docker/docker/contrib/
|
||||
RUN ./contrib/download-frozen-image-v2.sh /docker-frozen-images \
|
||||
buildpack-deps:jessie@sha256:dd86dced7c9cd2a724e779730f0a53f93b7ef42228d4344b25ce9a42a1486251 \
|
||||
busybox:latest@sha256:bbc3a03235220b170ba48a157dd097dd1379299370e1ed99ce976df0355d24f0 \
|
||||
debian:jessie@sha256:287a20c5f73087ab406e6b364833e3fb7b3ae63ca0eb3486555dc27ed32c6e60 \
|
||||
hello-world:latest@sha256:be0cd392e45be79ffeffa6b05338b98ebb16c87b255f48e297ec7f98e123905c
|
||||
s390x/buildpack-deps:jessie@sha256:552dec28146e4d2591fc0309aebdbac9e4fb1f335d90c70a14bbf72fb8bb1be5 \
|
||||
s390x/busybox:latest@sha256:e32f40c39ca596a4317392bd32809bb188c4ae5864ea827c3219c75c50069964 \
|
||||
s390x/debian:jessie@sha256:6994e3ffa5a1dabea09d536f350b3ed2715292cb469417c42a82b70fcbff7d32 \
|
||||
s390x/hello-world:latest@sha256:602db500fee63934292260e65c0c528128ad1c1c7c6497f95bbbac7d4d5312f1
|
||||
# See also ensureFrozenImagesLinux() in "integration-cli/fixtures_linux_daemon_test.go" (which needs to be updated when adding images to this list)
|
||||
|
||||
# Install tomlv, vndr, runc, containerd, tini, docker-proxy
|
||||
|
||||
@@ -27,8 +27,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
e2fsprogs \
|
||||
iptables \
|
||||
pkg-config \
|
||||
pigz \
|
||||
procps \
|
||||
xfsprogs \
|
||||
xz-utils \
|
||||
@@ -42,7 +40,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
# will need updating, to avoid errors. Ping #docker-maintainers on IRC
|
||||
# with a heads-up.
|
||||
# IMPORTANT: When updating this please note that stdlib archive/tar pkg is vendored
|
||||
ENV GO_VERSION 1.9.3
|
||||
ENV GO_VERSION 1.9.2
|
||||
RUN curl -fsSL "https://golang.org/dl/go${GO_VERSION}.linux-amd64.tar.gz" \
|
||||
| tar -xzC /usr/local
|
||||
ENV PATH /go/bin:/usr/local/go/bin:$PATH
|
||||
|
||||
@@ -161,7 +161,7 @@ SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPref
|
||||
# Environment variable notes:
|
||||
# - GO_VERSION must be consistent with 'Dockerfile' used by Linux.
|
||||
# - FROM_DOCKERFILE is used for detection of building within a container.
|
||||
ENV GO_VERSION=1.9.3 `
|
||||
ENV GO_VERSION=1.9.2 `
|
||||
GIT_VERSION=2.11.1 `
|
||||
GOPATH=C:\go `
|
||||
FROM_DOCKERFILE=1
|
||||
|
||||
@@ -3,7 +3,7 @@ package api
|
||||
// Common constants for daemon and client.
|
||||
const (
|
||||
// DefaultVersion of Current REST API
|
||||
DefaultVersion string = "1.36"
|
||||
DefaultVersion string = "1.35"
|
||||
|
||||
// NoBaseImageSpecifier is the symbol used by the FROM
|
||||
// command to specify that no base image is to be used.
|
||||
|
||||
@@ -43,11 +43,6 @@ type ErrNotModified interface {
|
||||
NotModified()
|
||||
}
|
||||
|
||||
// ErrAlreadyExists is a special case of ErrConflict which signals that the desired object already exists
|
||||
type ErrAlreadyExists interface {
|
||||
AlreadyExists()
|
||||
}
|
||||
|
||||
// ErrNotImplemented signals that the requested action/feature is not implemented on the system as configured.
|
||||
type ErrNotImplemented interface {
|
||||
NotImplemented()
|
||||
@@ -57,18 +52,3 @@ type ErrNotImplemented interface {
|
||||
type ErrUnknown interface {
|
||||
Unknown()
|
||||
}
|
||||
|
||||
// ErrCancelled signals that the action was cancelled.
|
||||
type ErrCancelled interface {
|
||||
Cancelled()
|
||||
}
|
||||
|
||||
// ErrDeadline signals that the deadline was reached before the action completed.
|
||||
type ErrDeadline interface {
|
||||
DeadlineExceeded()
|
||||
}
|
||||
|
||||
// ErrDataLoss indicates that data was lost or there is data corruption.
|
||||
type ErrDataLoss interface {
|
||||
DataLoss()
|
||||
}
|
||||
@@ -15,11 +15,7 @@ func getImplementer(err error) error {
|
||||
ErrForbidden,
|
||||
ErrSystem,
|
||||
ErrNotModified,
|
||||
ErrAlreadyExists,
|
||||
ErrNotImplemented,
|
||||
ErrCancelled,
|
||||
ErrDeadline,
|
||||
ErrDataLoss,
|
||||
ErrUnknown:
|
||||
return e
|
||||
case causer:
|
||||
@@ -77,12 +73,6 @@ func IsNotModified(err error) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsAlreadyExists returns if the passed in error is a AlreadyExists error
|
||||
func IsAlreadyExists(err error) bool {
|
||||
_, ok := getImplementer(err).(ErrAlreadyExists)
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsNotImplemented returns if the passed in error is an ErrNotImplemented
|
||||
func IsNotImplemented(err error) bool {
|
||||
_, ok := getImplementer(err).(ErrNotImplemented)
|
||||
@@ -94,21 +84,3 @@ func IsUnknown(err error) bool {
|
||||
_, ok := getImplementer(err).(ErrUnknown)
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsCancelled returns if the passed in error is an ErrCancelled
|
||||
func IsCancelled(err error) bool {
|
||||
_, ok := getImplementer(err).(ErrCancelled)
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsDeadline returns if the passed in error is an ErrDeadline
|
||||
func IsDeadline(err error) bool {
|
||||
_, ok := getImplementer(err).(ErrDeadline)
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsDataLoss returns if the passed in error is an ErrDataLoss
|
||||
func IsDataLoss(err error) bool {
|
||||
_, ok := getImplementer(err).(ErrDataLoss)
|
||||
return ok
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
// ImageComponent provides an interface for working with images
|
||||
type ImageComponent interface {
|
||||
SquashImage(from string, to string) (string, error)
|
||||
TagImageWithReference(image.ID, reference.Named) error
|
||||
TagImageWithReference(image.ID, string, reference.Named) error
|
||||
}
|
||||
|
||||
// Builder defines interface for running a build
|
||||
|
||||
@@ -3,9 +3,11 @@ package build
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"runtime"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/pkg/system"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@@ -33,7 +35,12 @@ func NewTagger(backend ImageComponent, stdout io.Writer, names []string) (*Tagge
|
||||
// TagImages creates image tags for the imageID
|
||||
func (bt *Tagger) TagImages(imageID image.ID) error {
|
||||
for _, rt := range bt.repoAndTags {
|
||||
if err := bt.imageComponent.TagImageWithReference(imageID, rt); err != nil {
|
||||
// TODO @jhowardmsft LCOW support. Will need revisiting.
|
||||
platform := runtime.GOOS
|
||||
if system.LCOWSupported() {
|
||||
platform = "linux"
|
||||
}
|
||||
if err := bt.imageComponent.TagImageWithReference(imageID, platform, rt); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(bt.stdout, "Successfully tagged %s\n", reference.FamiliarString(rt))
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/api/errdefs"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc"
|
||||
@@ -35,7 +35,7 @@ func GetHTTPErrorStatusCode(err error) int {
|
||||
statusCode = http.StatusNotFound
|
||||
case errdefs.IsInvalidParameter(err):
|
||||
statusCode = http.StatusBadRequest
|
||||
case errdefs.IsConflict(err) || errdefs.IsAlreadyExists(err):
|
||||
case errdefs.IsConflict(err):
|
||||
statusCode = http.StatusConflict
|
||||
case errdefs.IsUnauthorized(err):
|
||||
statusCode = http.StatusUnauthorized
|
||||
@@ -47,7 +47,7 @@ func GetHTTPErrorStatusCode(err error) int {
|
||||
statusCode = http.StatusNotModified
|
||||
case errdefs.IsNotImplemented(err):
|
||||
statusCode = http.StatusNotImplemented
|
||||
case errdefs.IsSystem(err) || errdefs.IsUnknown(err) || errdefs.IsDataLoss(err) || errdefs.IsDeadline(err) || errdefs.IsCancelled(err):
|
||||
case errdefs.IsSystem(err) || errdefs.IsUnknown(err):
|
||||
statusCode = http.StatusInternalServerError
|
||||
default:
|
||||
statusCode = statusCodeFromGRPCError(err)
|
||||
|
||||
@@ -6,16 +6,13 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
// APIVersionKey is the client's requested API version.
|
||||
const APIVersionKey contextKey = "api-version"
|
||||
const APIVersionKey = "api-version"
|
||||
|
||||
// APIFunc is an adapter to allow the use of ordinary functions as Docker API endpoints.
|
||||
// Any function that has the appropriate signature can be registered as an API endpoint (e.g. getVersion).
|
||||
@@ -46,6 +43,20 @@ func CloseStreams(streams ...interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
type validationError struct {
|
||||
cause error
|
||||
}
|
||||
|
||||
func (e validationError) Error() string {
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
func (e validationError) Cause() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
func (e validationError) InvalidParameter() {}
|
||||
|
||||
// CheckForJSON makes sure that the request's Content-Type is application/json.
|
||||
func CheckForJSON(r *http.Request) error {
|
||||
ct := r.Header.Get("Content-Type")
|
||||
@@ -61,7 +72,7 @@ func CheckForJSON(r *http.Request) error {
|
||||
if matchesContentType(ct, "application/json") {
|
||||
return nil
|
||||
}
|
||||
return errdefs.InvalidParameter(errors.Errorf("Content-Type specified (%s) must be 'application/json'", ct))
|
||||
return validationError{errors.Errorf("Content-Type specified (%s) must be 'application/json'", ct)}
|
||||
}
|
||||
|
||||
// ParseForm ensures the request form is parsed even with invalid content types.
|
||||
@@ -71,7 +82,7 @@ func ParseForm(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
if err := r.ParseForm(); err != nil && !strings.HasPrefix(err.Error(), "mime:") {
|
||||
return errdefs.InvalidParameter(err)
|
||||
return validationError{err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"net/http"
|
||||
"runtime"
|
||||
|
||||
"github.com/docker/docker/api/server/httputils"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
@@ -58,7 +57,8 @@ func (v VersionMiddleware) WrapHandler(handler func(ctx context.Context, w http.
|
||||
if versions.GreaterThan(apiVersion, v.defaultVersion) {
|
||||
return versionUnsupportedError{version: apiVersion, maxVersion: v.defaultVersion}
|
||||
}
|
||||
ctx = context.WithValue(ctx, httputils.APIVersionKey, apiVersion)
|
||||
// nolint: golint
|
||||
ctx = context.WithValue(ctx, "api-version", apiVersion)
|
||||
return handler(ctx, w, r, vars)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/server/httputils"
|
||||
@@ -11,62 +12,32 @@ import (
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestVersionMiddlewareVersion(t *testing.T) {
|
||||
defaultVersion := "1.10.0"
|
||||
minVersion := "1.2.0"
|
||||
expectedVersion := defaultVersion
|
||||
func TestVersionMiddleware(t *testing.T) {
|
||||
handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
v := httputils.VersionFromContext(ctx)
|
||||
assert.Equal(t, expectedVersion, v)
|
||||
if httputils.VersionFromContext(ctx) == "" {
|
||||
t.Fatal("Expected version, got empty string")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
defaultVersion := "1.10.0"
|
||||
minVersion := "1.2.0"
|
||||
m := NewVersionMiddleware(defaultVersion, defaultVersion, minVersion)
|
||||
h := m.WrapHandler(handler)
|
||||
|
||||
req, _ := http.NewRequest("GET", "/containers/json", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
ctx := context.Background()
|
||||
|
||||
tests := []struct {
|
||||
reqVersion string
|
||||
expectedVersion string
|
||||
errString string
|
||||
}{
|
||||
{
|
||||
expectedVersion: "1.10.0",
|
||||
},
|
||||
{
|
||||
reqVersion: "1.9.0",
|
||||
expectedVersion: "1.9.0",
|
||||
},
|
||||
{
|
||||
reqVersion: "0.1",
|
||||
errString: "client version 0.1 is too old. Minimum supported API version is 1.2.0, please upgrade your client to a newer version",
|
||||
},
|
||||
{
|
||||
reqVersion: "9999.9999",
|
||||
errString: "client version 9999.9999 is too new. Maximum supported API version is 1.10.0",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
expectedVersion = test.expectedVersion
|
||||
|
||||
err := h(ctx, resp, req, map[string]string{"version": test.reqVersion})
|
||||
|
||||
if test.errString != "" {
|
||||
assert.EqualError(t, err, test.errString)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
if err := h(ctx, resp, req, map[string]string{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionMiddlewareWithErrorsReturnsHeaders(t *testing.T) {
|
||||
func TestVersionMiddlewareVersionTooOld(t *testing.T) {
|
||||
handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
v := httputils.VersionFromContext(ctx)
|
||||
assert.NotEmpty(t, v)
|
||||
if httputils.VersionFromContext(ctx) == "" {
|
||||
t.Fatal("Expected version, got empty string")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -81,8 +52,58 @@ func TestVersionMiddlewareWithErrorsReturnsHeaders(t *testing.T) {
|
||||
|
||||
vars := map[string]string{"version": "0.1"}
|
||||
err := h(ctx, resp, req, vars)
|
||||
assert.Error(t, err)
|
||||
|
||||
if !strings.Contains(err.Error(), "client version 0.1 is too old. Minimum supported API version is 1.2.0") {
|
||||
t.Fatalf("Expected too old client error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionMiddlewareVersionTooNew(t *testing.T) {
|
||||
handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
if httputils.VersionFromContext(ctx) == "" {
|
||||
t.Fatal("Expected version, got empty string")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
defaultVersion := "1.10.0"
|
||||
minVersion := "1.2.0"
|
||||
m := NewVersionMiddleware(defaultVersion, defaultVersion, minVersion)
|
||||
h := m.WrapHandler(handler)
|
||||
|
||||
req, _ := http.NewRequest("GET", "/containers/json", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
ctx := context.Background()
|
||||
|
||||
vars := map[string]string{"version": "9999.9999"}
|
||||
err := h(ctx, resp, req, vars)
|
||||
|
||||
if !strings.Contains(err.Error(), "client version 9999.9999 is too new. Maximum supported API version is 1.10.0") {
|
||||
t.Fatalf("Expected too new client error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionMiddlewareWithErrorsReturnsHeaders(t *testing.T) {
|
||||
handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
if httputils.VersionFromContext(ctx) == "" {
|
||||
t.Fatal("Expected version, got empty string")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
defaultVersion := "1.10.0"
|
||||
minVersion := "1.2.0"
|
||||
m := NewVersionMiddleware(defaultVersion, defaultVersion, minVersion)
|
||||
h := m.WrapHandler(handler)
|
||||
|
||||
req, _ := http.NewRequest("GET", "/containers/json", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
ctx := context.Background()
|
||||
|
||||
vars := map[string]string{"version": "0.1"}
|
||||
err := h(ctx, resp, req, vars)
|
||||
|
||||
assert.Error(t, err)
|
||||
hdr := resp.Result().Header
|
||||
assert.Contains(t, hdr.Get("Server"), "Docker/"+defaultVersion)
|
||||
assert.Contains(t, hdr.Get("Server"), runtime.GOOS)
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"github.com/docker/docker/api/types/backend"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/docker/docker/pkg/progress"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
@@ -84,7 +83,7 @@ func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBui
|
||||
}
|
||||
p := system.ParsePlatform(apiPlatform)
|
||||
if err := system.ValidatePlatform(p); err != nil {
|
||||
return nil, errdefs.InvalidParameter(errors.Errorf("invalid platform: %s", err))
|
||||
return nil, validationError{fmt.Errorf("invalid platform: %s", err)}
|
||||
}
|
||||
options.Platform = p.OS
|
||||
}
|
||||
@@ -105,14 +104,14 @@ func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBui
|
||||
}
|
||||
|
||||
if runtime.GOOS != "windows" && options.SecurityOpt != nil {
|
||||
return nil, errdefs.InvalidParameter(errors.New("The daemon on this platform does not support setting security options on build"))
|
||||
return nil, validationError{fmt.Errorf("The daemon on this platform does not support setting security options on build")}
|
||||
}
|
||||
|
||||
var buildUlimits = []*units.Ulimit{}
|
||||
ulimitsJSON := r.FormValue("ulimits")
|
||||
if ulimitsJSON != "" {
|
||||
if err := json.Unmarshal([]byte(ulimitsJSON), &buildUlimits); err != nil {
|
||||
return nil, errors.Wrap(errdefs.InvalidParameter(err), "error reading ulimit settings")
|
||||
return nil, errors.Wrap(validationError{err}, "error reading ulimit settings")
|
||||
}
|
||||
options.Ulimits = buildUlimits
|
||||
}
|
||||
@@ -133,7 +132,7 @@ func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBui
|
||||
if buildArgsJSON != "" {
|
||||
var buildArgs = map[string]*string{}
|
||||
if err := json.Unmarshal([]byte(buildArgsJSON), &buildArgs); err != nil {
|
||||
return nil, errors.Wrap(errdefs.InvalidParameter(err), "error reading build args")
|
||||
return nil, errors.Wrap(validationError{err}, "error reading build args")
|
||||
}
|
||||
options.BuildArgs = buildArgs
|
||||
}
|
||||
@@ -142,7 +141,7 @@ func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBui
|
||||
if labelsJSON != "" {
|
||||
var labels = map[string]string{}
|
||||
if err := json.Unmarshal([]byte(labelsJSON), &labels); err != nil {
|
||||
return nil, errors.Wrap(errdefs.InvalidParameter(err), "error reading labels")
|
||||
return nil, errors.Wrap(validationError{err}, "error reading labels")
|
||||
}
|
||||
options.Labels = labels
|
||||
}
|
||||
@@ -168,6 +167,16 @@ func (br *buildRouter) postPrune(ctx context.Context, w http.ResponseWriter, r *
|
||||
return httputils.WriteJSON(w, http.StatusOK, report)
|
||||
}
|
||||
|
||||
type validationError struct {
|
||||
cause error
|
||||
}
|
||||
|
||||
func (e validationError) Error() string {
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
func (e validationError) InvalidParameter() {}
|
||||
|
||||
func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
var (
|
||||
notVerboseBuffer = bytes.NewBuffer(nil)
|
||||
@@ -201,7 +210,7 @@ func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *
|
||||
buildOptions.AuthConfigs = getAuthConfigs(r.Header)
|
||||
|
||||
if buildOptions.Squash && !br.daemon.HasExperimental() {
|
||||
return errdefs.InvalidParameter(errors.New("squash is only supported with experimental mode"))
|
||||
return validationError{errors.New("squash is only supported with experimental mode")}
|
||||
}
|
||||
|
||||
out := io.Writer(output)
|
||||
|
||||
@@ -5,6 +5,20 @@ import (
|
||||
"github.com/docker/docker/api/server/router"
|
||||
)
|
||||
|
||||
type validationError struct {
|
||||
cause error
|
||||
}
|
||||
|
||||
func (e validationError) Error() string {
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
func (e validationError) Cause() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
func (e validationError) InvalidParameter() {}
|
||||
|
||||
// containerRouter is a router to talk with the container controller
|
||||
type containerRouter struct {
|
||||
backend Backend
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"github.com/docker/docker/api/errdefs"
|
||||
"github.com/docker/docker/api/server/httputils"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/backend"
|
||||
@@ -15,7 +16,6 @@ import (
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
containerpkg "github.com/docker/docker/container"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/docker/docker/pkg/signal"
|
||||
"github.com/pkg/errors"
|
||||
@@ -88,7 +88,7 @@ func (s *containerRouter) getContainersLogs(ctx context.Context, w http.Response
|
||||
// with the appropriate status code.
|
||||
stdout, stderr := httputils.BoolValue(r, "stdout"), httputils.BoolValue(r, "stderr")
|
||||
if !(stdout || stderr) {
|
||||
return errdefs.InvalidParameter(errors.New("Bad parameters: you must choose at least one stream"))
|
||||
return validationError{errors.New("Bad parameters: you must choose at least one stream")}
|
||||
}
|
||||
|
||||
containerName := vars["name"]
|
||||
@@ -203,7 +203,7 @@ func (s *containerRouter) postContainersKill(ctx context.Context, w http.Respons
|
||||
if sigStr := r.Form.Get("signal"); sigStr != "" {
|
||||
var err error
|
||||
if sig, err = signal.ParseSignal(sigStr); err != nil {
|
||||
return errdefs.InvalidParameter(err)
|
||||
return validationError{err}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -468,11 +468,11 @@ func (s *containerRouter) postContainersResize(ctx context.Context, w http.Respo
|
||||
|
||||
height, err := strconv.Atoi(r.Form.Get("h"))
|
||||
if err != nil {
|
||||
return errdefs.InvalidParameter(err)
|
||||
return validationError{err}
|
||||
}
|
||||
width, err := strconv.Atoi(r.Form.Get("w"))
|
||||
if err != nil {
|
||||
return errdefs.InvalidParameter(err)
|
||||
return validationError{err}
|
||||
}
|
||||
|
||||
return s.backend.ContainerResize(vars["name"], height, width)
|
||||
@@ -490,7 +490,7 @@ func (s *containerRouter) postContainersAttach(ctx context.Context, w http.Respo
|
||||
|
||||
hijacker, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
return errdefs.InvalidParameter(errors.Errorf("error attaching to container %s, hijack connection missing", containerName))
|
||||
return validationError{errors.Errorf("error attaching to container %s, hijack connection missing", containerName)}
|
||||
}
|
||||
|
||||
setupStreams := func() (io.ReadCloser, io.Writer, io.Writer, error) {
|
||||
@@ -611,7 +611,7 @@ func (s *containerRouter) postContainersPrune(ctx context.Context, w http.Respon
|
||||
|
||||
pruneFilters, err := filters.FromJSON(r.Form.Get("filters"))
|
||||
if err != nil {
|
||||
return errdefs.InvalidParameter(err)
|
||||
return validationError{err}
|
||||
}
|
||||
|
||||
pruneReport, err := s.backend.ContainersPrune(ctx, pruneFilters)
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/docker/docker/api/server/httputils"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/context"
|
||||
@@ -138,11 +137,11 @@ func (s *containerRouter) postContainerExecResize(ctx context.Context, w http.Re
|
||||
}
|
||||
height, err := strconv.Atoi(r.Form.Get("h"))
|
||||
if err != nil {
|
||||
return errdefs.InvalidParameter(err)
|
||||
return validationError{err}
|
||||
}
|
||||
width, err := strconv.Atoi(r.Form.Get("w"))
|
||||
if err != nil {
|
||||
return errdefs.InvalidParameter(err)
|
||||
return validationError{err}
|
||||
}
|
||||
|
||||
return s.backend.ContainerExecResize(vars["name"], height, width)
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"github.com/docker/docker/api/types/backend"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/pkg/system"
|
||||
@@ -145,6 +144,20 @@ func (s *imageRouter) postImagesCreate(ctx context.Context, w http.ResponseWrite
|
||||
return nil
|
||||
}
|
||||
|
||||
type validationError struct {
|
||||
cause error
|
||||
}
|
||||
|
||||
func (e validationError) Error() string {
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
func (e validationError) Cause() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
func (validationError) InvalidParameter() {}
|
||||
|
||||
func (s *imageRouter) postImagesPush(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
metaHeaders := map[string][]string{}
|
||||
for k, v := range r.Header {
|
||||
@@ -168,7 +181,7 @@ func (s *imageRouter) postImagesPush(ctx context.Context, w http.ResponseWriter,
|
||||
} else {
|
||||
// the old format is supported for compatibility if there was no authConfig header
|
||||
if err := json.NewDecoder(r.Body).Decode(authConfig); err != nil {
|
||||
return errors.Wrap(errdefs.InvalidParameter(err), "Bad parameters and missing X-Registry-Auth")
|
||||
return errors.Wrap(validationError{err}, "Bad parameters and missing X-Registry-Auth")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,6 @@ type Backend interface {
|
||||
CreateNetwork(nc types.NetworkCreateRequest) (*types.NetworkCreateResponse, error)
|
||||
ConnectContainerToNetwork(containerName, networkName string, endpointConfig *network.EndpointSettings) error
|
||||
DisconnectContainerFromNetwork(containerName string, networkName string, force bool) error
|
||||
DeleteNetwork(networkID string) error
|
||||
DeleteNetwork(name string) error
|
||||
NetworksPrune(ctx context.Context, pruneFilters filters.Args) (*types.NetworksPruneReport, error)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/docker/libnetwork"
|
||||
netconst "github.com/docker/libnetwork/datastore"
|
||||
"github.com/docker/libnetwork/networkdb"
|
||||
@@ -101,8 +100,22 @@ func (e ambigousResultsError) Error() string {
|
||||
|
||||
func (ambigousResultsError) InvalidParameter() {}
|
||||
|
||||
type conflictError struct {
|
||||
cause error
|
||||
}
|
||||
|
||||
func (e conflictError) Error() string {
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
func (e conflictError) Cause() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
func (e conflictError) Conflict() {}
|
||||
|
||||
func nameConflict(name string) error {
|
||||
return errdefs.Conflict(libnetwork.NetworkNameError(name))
|
||||
return conflictError{libnetwork.NetworkNameError(name)}
|
||||
}
|
||||
|
||||
func (n *networkRouter) getNetwork(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
@@ -275,10 +288,6 @@ func (n *networkRouter) postNetworkConnect(ctx context.Context, w http.ResponseW
|
||||
return err
|
||||
}
|
||||
|
||||
// Unlike other operations, we does not check ambiguity of the name/ID here.
|
||||
// The reason is that, In case of attachable network in swarm scope, the actual local network
|
||||
// may not be available at the time. At the same time, inside daemon `ConnectContainerToNetwork`
|
||||
// does the ambiguity check anyway. Therefore, passing the name to daemon would be enough.
|
||||
return n.backend.ConnectContainerToNetwork(connect.Container, vars["id"], connect.EndpointConfig)
|
||||
}
|
||||
|
||||
@@ -303,19 +312,15 @@ func (n *networkRouter) deleteNetwork(ctx context.Context, w http.ResponseWriter
|
||||
if err := httputils.ParseForm(r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nw, err := n.findUniqueNetwork(vars["id"])
|
||||
if err != nil {
|
||||
return err
|
||||
if _, err := n.cluster.GetNetwork(vars["id"]); err == nil {
|
||||
if err = n.cluster.RemoveNetwork(vars["id"]); err != nil {
|
||||
return err
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return nil
|
||||
}
|
||||
if nw.Scope == "swarm" {
|
||||
if err = n.cluster.RemoveNetwork(nw.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := n.backend.DeleteNetwork(nw.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := n.backend.DeleteNetwork(vars["id"]); err != nil {
|
||||
return err
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return nil
|
||||
@@ -513,79 +518,3 @@ func (n *networkRouter) postNetworksPrune(ctx context.Context, w http.ResponseWr
|
||||
}
|
||||
return httputils.WriteJSON(w, http.StatusOK, pruneReport)
|
||||
}
|
||||
|
||||
// findUniqueNetwork will search network across different scopes (both local and swarm).
|
||||
// NOTE: This findUniqueNetwork is different from FindNetwork in the daemon.
|
||||
// In case multiple networks have duplicate names, return error.
|
||||
// First find based on full ID, return immediately once one is found.
|
||||
// If a network appears both in swarm and local, assume it is in local first
|
||||
// For full name and partial ID, save the result first, and process later
|
||||
// in case multiple records was found based on the same term
|
||||
// TODO (yongtang): should we wrap with version here for backward compatibility?
|
||||
func (n *networkRouter) findUniqueNetwork(term string) (types.NetworkResource, error) {
|
||||
listByFullName := map[string]types.NetworkResource{}
|
||||
listByPartialID := map[string]types.NetworkResource{}
|
||||
|
||||
nw := n.backend.GetNetworks()
|
||||
for _, network := range nw {
|
||||
if network.ID() == term {
|
||||
return *n.buildDetailedNetworkResources(network, false), nil
|
||||
|
||||
}
|
||||
if network.Name() == term && !network.Info().Ingress() {
|
||||
// No need to check the ID collision here as we are still in
|
||||
// local scope and the network ID is unique in this scope.
|
||||
listByFullName[network.ID()] = *n.buildDetailedNetworkResources(network, false)
|
||||
}
|
||||
if strings.HasPrefix(network.ID(), term) {
|
||||
// No need to check the ID collision here as we are still in
|
||||
// local scope and the network ID is unique in this scope.
|
||||
listByPartialID[network.ID()] = *n.buildDetailedNetworkResources(network, false)
|
||||
}
|
||||
}
|
||||
|
||||
nr, _ := n.cluster.GetNetworks()
|
||||
for _, network := range nr {
|
||||
if network.ID == term {
|
||||
return network, nil
|
||||
}
|
||||
if network.Name == term {
|
||||
// Check the ID collision as we are in swarm scope here, and
|
||||
// the map (of the listByFullName) may have already had a
|
||||
// network with the same ID (from local scope previously)
|
||||
if _, ok := listByFullName[network.ID]; !ok {
|
||||
listByFullName[network.ID] = network
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(network.ID, term) {
|
||||
// Check the ID collision as we are in swarm scope here, and
|
||||
// the map (of the listByPartialID) may have already had a
|
||||
// network with the same ID (from local scope previously)
|
||||
if _, ok := listByPartialID[network.ID]; !ok {
|
||||
listByPartialID[network.ID] = network
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find based on full name, returns true only if no duplicates
|
||||
if len(listByFullName) == 1 {
|
||||
for _, v := range listByFullName {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
if len(listByFullName) > 1 {
|
||||
return types.NetworkResource{}, errdefs.InvalidParameter(errors.Errorf("network %s is ambiguous (%d matches found based on name)", term, len(listByFullName)))
|
||||
}
|
||||
|
||||
// Find based on partial ID, returns true only if no duplicates
|
||||
if len(listByPartialID) == 1 {
|
||||
for _, v := range listByPartialID {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
if len(listByPartialID) > 1 {
|
||||
return types.NetworkResource{}, errdefs.InvalidParameter(errors.Errorf("network %s is ambiguous (%d matches found based on ID prefix)", term, len(listByPartialID)))
|
||||
}
|
||||
|
||||
return types.NetworkResource{}, errdefs.NotFound(libnetwork.ErrNoSuchNetwork(term))
|
||||
}
|
||||
|
||||
@@ -3,14 +3,27 @@ package session
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/errdefs"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type invalidRequest struct {
|
||||
cause error
|
||||
}
|
||||
|
||||
func (e invalidRequest) Error() string {
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
func (e invalidRequest) Cause() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
func (e invalidRequest) InvalidParameter() {}
|
||||
|
||||
func (sr *sessionRouter) startSession(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
err := sr.backend.HandleHTTPRequest(ctx, w, r)
|
||||
if err != nil {
|
||||
return errdefs.InvalidParameter(err)
|
||||
return invalidRequest{err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
types "github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/context"
|
||||
@@ -58,6 +57,20 @@ func (sr *swarmRouter) inspectCluster(ctx context.Context, w http.ResponseWriter
|
||||
return httputils.WriteJSON(w, http.StatusOK, swarm)
|
||||
}
|
||||
|
||||
type invalidRequestError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (e invalidRequestError) Error() string {
|
||||
return e.err.Error()
|
||||
}
|
||||
|
||||
func (e invalidRequestError) Cause() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
func (e invalidRequestError) InvalidParameter() {}
|
||||
|
||||
func (sr *swarmRouter) updateCluster(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
var swarm types.Spec
|
||||
if err := json.NewDecoder(r.Body).Decode(&swarm); err != nil {
|
||||
@@ -68,7 +81,7 @@ func (sr *swarmRouter) updateCluster(ctx context.Context, w http.ResponseWriter,
|
||||
version, err := strconv.ParseUint(rawVersion, 10, 64)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("invalid swarm version '%s': %v", rawVersion, err)
|
||||
return errdefs.InvalidParameter(err)
|
||||
return invalidRequestError{err}
|
||||
}
|
||||
|
||||
var flags types.UpdateFlags
|
||||
@@ -77,7 +90,7 @@ func (sr *swarmRouter) updateCluster(ctx context.Context, w http.ResponseWriter,
|
||||
rot, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("invalid value for rotateWorkerToken: %s", value)
|
||||
return errdefs.InvalidParameter(err)
|
||||
return invalidRequestError{err}
|
||||
}
|
||||
|
||||
flags.RotateWorkerToken = rot
|
||||
@@ -87,7 +100,7 @@ func (sr *swarmRouter) updateCluster(ctx context.Context, w http.ResponseWriter,
|
||||
rot, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("invalid value for rotateManagerToken: %s", value)
|
||||
return errdefs.InvalidParameter(err)
|
||||
return invalidRequestError{err}
|
||||
}
|
||||
|
||||
flags.RotateManagerToken = rot
|
||||
@@ -96,7 +109,7 @@ func (sr *swarmRouter) updateCluster(ctx context.Context, w http.ResponseWriter,
|
||||
if value := r.URL.Query().Get("rotateManagerUnlockKey"); value != "" {
|
||||
rot, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return errdefs.InvalidParameter(fmt.Errorf("invalid value for rotateManagerUnlockKey: %s", value))
|
||||
return invalidRequestError{fmt.Errorf("invalid value for rotateManagerUnlockKey: %s", value)}
|
||||
}
|
||||
|
||||
flags.RotateManagerUnlockKey = rot
|
||||
@@ -140,7 +153,7 @@ func (sr *swarmRouter) getServices(ctx context.Context, w http.ResponseWriter, r
|
||||
}
|
||||
filter, err := filters.FromJSON(r.Form.Get("filters"))
|
||||
if err != nil {
|
||||
return errdefs.InvalidParameter(err)
|
||||
return invalidRequestError{err}
|
||||
}
|
||||
|
||||
services, err := sr.backend.GetServices(basictypes.ServiceListOptions{Filters: filter})
|
||||
@@ -159,7 +172,7 @@ func (sr *swarmRouter) getService(ctx context.Context, w http.ResponseWriter, r
|
||||
insertDefaults, err = strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("invalid value for insertDefaults: %s", value)
|
||||
return errors.Wrapf(errdefs.InvalidParameter(err), "invalid value for insertDefaults: %s", value)
|
||||
return errors.Wrapf(invalidRequestError{err}, "invalid value for insertDefaults: %s", value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,7 +218,7 @@ func (sr *swarmRouter) updateService(ctx context.Context, w http.ResponseWriter,
|
||||
version, err := strconv.ParseUint(rawVersion, 10, 64)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("invalid service version '%s': %v", rawVersion, err)
|
||||
return errdefs.InvalidParameter(err)
|
||||
return invalidRequestError{err}
|
||||
}
|
||||
|
||||
var flags basictypes.ServiceUpdateOptions
|
||||
@@ -298,7 +311,7 @@ func (sr *swarmRouter) updateNode(ctx context.Context, w http.ResponseWriter, r
|
||||
version, err := strconv.ParseUint(rawVersion, 10, 64)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("invalid node version '%s': %v", rawVersion, err)
|
||||
return errdefs.InvalidParameter(err)
|
||||
return invalidRequestError{err}
|
||||
}
|
||||
|
||||
if err := sr.backend.UpdateNode(vars["id"], version, node); err != nil {
|
||||
@@ -404,13 +417,13 @@ func (sr *swarmRouter) getSecret(ctx context.Context, w http.ResponseWriter, r *
|
||||
func (sr *swarmRouter) updateSecret(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
var secret types.SecretSpec
|
||||
if err := json.NewDecoder(r.Body).Decode(&secret); err != nil {
|
||||
return errdefs.InvalidParameter(err)
|
||||
return invalidRequestError{err}
|
||||
}
|
||||
|
||||
rawVersion := r.URL.Query().Get("version")
|
||||
version, err := strconv.ParseUint(rawVersion, 10, 64)
|
||||
if err != nil {
|
||||
return errdefs.InvalidParameter(fmt.Errorf("invalid secret version"))
|
||||
return invalidRequestError{fmt.Errorf("invalid secret version")}
|
||||
}
|
||||
|
||||
id := vars["id"]
|
||||
@@ -471,13 +484,13 @@ func (sr *swarmRouter) getConfig(ctx context.Context, w http.ResponseWriter, r *
|
||||
func (sr *swarmRouter) updateConfig(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
var config types.ConfigSpec
|
||||
if err := json.NewDecoder(r.Body).Decode(&config); err != nil {
|
||||
return errdefs.InvalidParameter(err)
|
||||
return invalidRequestError{err}
|
||||
}
|
||||
|
||||
rawVersion := r.URL.Query().Get("version")
|
||||
version, err := strconv.ParseUint(rawVersion, 10, 64)
|
||||
if err != nil {
|
||||
return errdefs.InvalidParameter(fmt.Errorf("invalid config version"))
|
||||
return invalidRequestError{fmt.Errorf("invalid config version")}
|
||||
}
|
||||
|
||||
id := vars["id"]
|
||||
|
||||
@@ -2,14 +2,11 @@ package volume
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/api/server/httputils"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
volumetypes "github.com/docker/docker/api/types/volume"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
@@ -48,9 +45,6 @@ func (v *volumeRouter) postVolumesCreate(ctx context.Context, w http.ResponseWri
|
||||
|
||||
var req volumetypes.VolumesCreateBody
|
||||
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"))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -19,10 +19,10 @@ produces:
|
||||
consumes:
|
||||
- "application/json"
|
||||
- "text/plain"
|
||||
basePath: "/v1.36"
|
||||
basePath: "/v1.35"
|
||||
info:
|
||||
title: "Docker Engine API"
|
||||
version: "1.36"
|
||||
version: "1.35"
|
||||
x-logo:
|
||||
url: "https://docs.docker.com/images/logo-docker-main.png"
|
||||
description: |
|
||||
@@ -49,8 +49,8 @@ info:
|
||||
the URL is not supported by the daemon, a HTTP `400 Bad Request` error message
|
||||
is returned.
|
||||
|
||||
If you omit the version-prefix, the current version of the API (v1.36) is used.
|
||||
For example, calling `/info` is the same as calling `/v1.36/info`. Using the
|
||||
If you omit the version-prefix, the current version of the API (v1.35) is used.
|
||||
For example, calling `/info` is the same as calling `/v1.35/info`. Using the
|
||||
API without a version-prefix is deprecated and will be removed in a future release.
|
||||
|
||||
Engine releases in the near future should support this version of the API,
|
||||
@@ -1496,23 +1496,10 @@ definitions:
|
||||
type: "string"
|
||||
progressDetail:
|
||||
$ref: "#/definitions/ProgressDetail"
|
||||
aux:
|
||||
$ref: "#/definitions/ImageID"
|
||||
|
||||
ImageID:
|
||||
type: "object"
|
||||
description: "Image ID or Digest"
|
||||
properties:
|
||||
ID:
|
||||
type: "string"
|
||||
example:
|
||||
ID: "sha256:85f05633ddc1c50679be2b16a0479ab6f7637f8884e0cfe0f4d20e1ebb3d6e7c"
|
||||
|
||||
CreateImageInfo:
|
||||
type: "object"
|
||||
properties:
|
||||
id:
|
||||
type: "string"
|
||||
error:
|
||||
type: "string"
|
||||
status:
|
||||
@@ -1545,9 +1532,9 @@ definitions:
|
||||
ProgressDetail:
|
||||
type: "object"
|
||||
properties:
|
||||
current:
|
||||
code:
|
||||
type: "integer"
|
||||
total:
|
||||
message:
|
||||
type: "integer"
|
||||
|
||||
ErrorResponse:
|
||||
@@ -4502,8 +4489,6 @@ paths:
|
||||
description: "Container created successfully"
|
||||
schema:
|
||||
type: "object"
|
||||
title: "ContainerCreateResponse"
|
||||
description: "OK response to ContainerCreate operation"
|
||||
required: [Id, Warnings]
|
||||
properties:
|
||||
Id:
|
||||
@@ -4552,7 +4537,6 @@ paths:
|
||||
description: "no error"
|
||||
schema:
|
||||
type: "object"
|
||||
title: "ContainerInspectResponse"
|
||||
properties:
|
||||
Id:
|
||||
description: "The ID of the container"
|
||||
@@ -4837,8 +4821,6 @@ paths:
|
||||
description: "no error"
|
||||
schema:
|
||||
type: "object"
|
||||
title: "ContainerTopResponse"
|
||||
description: "OK response to ContainerTop operation"
|
||||
properties:
|
||||
Titles:
|
||||
description: "The ps column titles"
|
||||
@@ -4999,8 +4981,6 @@ paths:
|
||||
items:
|
||||
type: "object"
|
||||
x-go-name: "ContainerChangeResponseItem"
|
||||
title: "ContainerChangeResponseItem"
|
||||
description: "change item in response to ContainerChanges operation"
|
||||
required: [Path, Kind]
|
||||
properties:
|
||||
Path:
|
||||
@@ -5379,8 +5359,6 @@ paths:
|
||||
description: "The container has been updated."
|
||||
schema:
|
||||
type: "object"
|
||||
title: "ContainerUpdateResponse"
|
||||
description: "OK response to ContainerUpdate operation"
|
||||
properties:
|
||||
Warnings:
|
||||
type: "array"
|
||||
@@ -5734,8 +5712,6 @@ paths:
|
||||
description: "The container has exit."
|
||||
schema:
|
||||
type: "object"
|
||||
title: "ContainerWaitResponse"
|
||||
description: "OK response to ContainerWait operation"
|
||||
required: [StatusCode]
|
||||
properties:
|
||||
StatusCode:
|
||||
@@ -5981,7 +5957,6 @@ paths:
|
||||
description: "No error"
|
||||
schema:
|
||||
type: "object"
|
||||
title: "ContainerPruneResponse"
|
||||
properties:
|
||||
ContainersDeleted:
|
||||
description: "Container IDs that were deleted"
|
||||
@@ -6238,7 +6213,6 @@ paths:
|
||||
description: "No error"
|
||||
schema:
|
||||
type: "object"
|
||||
title: "BuildPruneResponse"
|
||||
properties:
|
||||
SpaceReclaimed:
|
||||
description: "Disk space reclaimed in bytes"
|
||||
@@ -6424,8 +6398,6 @@ paths:
|
||||
items:
|
||||
type: "object"
|
||||
x-go-name: HistoryResponseItem
|
||||
title: "HistoryResponseItem"
|
||||
description: "individual image layer information in response to ImageHistory operation"
|
||||
required: [Id, Created, CreatedBy, Tags, Size, Comment]
|
||||
properties:
|
||||
Id:
|
||||
@@ -6632,7 +6604,6 @@ paths:
|
||||
type: "array"
|
||||
items:
|
||||
type: "object"
|
||||
title: "ImageSearchResponseItem"
|
||||
properties:
|
||||
description:
|
||||
type: "string"
|
||||
@@ -6708,7 +6679,6 @@ paths:
|
||||
description: "No error"
|
||||
schema:
|
||||
type: "object"
|
||||
title: "ImagePruneResponse"
|
||||
properties:
|
||||
ImagesDeleted:
|
||||
description: "Images that were deleted"
|
||||
@@ -6736,7 +6706,6 @@ paths:
|
||||
description: "An identity token was generated successfully."
|
||||
schema:
|
||||
type: "object"
|
||||
title: "SystemAuthResponse"
|
||||
required: [Status]
|
||||
properties:
|
||||
Status:
|
||||
@@ -6791,7 +6760,6 @@ paths:
|
||||
description: "no error"
|
||||
schema:
|
||||
type: "object"
|
||||
title: "SystemVersionResponse"
|
||||
properties:
|
||||
Platform:
|
||||
type: "object"
|
||||
@@ -6944,7 +6912,7 @@ paths:
|
||||
|
||||
Various objects within Docker report events when something happens to them.
|
||||
|
||||
Containers report these events: `attach`, `commit`, `copy`, `create`, `destroy`, `detach`, `die`, `exec_create`, `exec_detach`, `exec_start`, `exec_die`, `export`, `health_status`, `kill`, `oom`, `pause`, `rename`, `resize`, `restart`, `start`, `stop`, `top`, `unpause`, and `update`
|
||||
Containers report these events: `attach`, `commit`, `copy`, `create`, `destroy`, `detach`, `die`, `exec_create`, `exec_detach`, `exec_start`, `export`, `health_status`, `kill`, `oom`, `pause`, `rename`, `resize`, `restart`, `start`, `stop`, `top`, `unpause`, and `update`
|
||||
|
||||
Images report these events: `delete`, `import`, `load`, `pull`, `push`, `save`, `tag`, and `untag`
|
||||
|
||||
@@ -6970,7 +6938,6 @@ paths:
|
||||
description: "no error"
|
||||
schema:
|
||||
type: "object"
|
||||
title: "SystemEventsResponse"
|
||||
properties:
|
||||
Type:
|
||||
description: "The type of object emitting the event"
|
||||
@@ -7054,7 +7021,6 @@ paths:
|
||||
description: "no error"
|
||||
schema:
|
||||
type: "object"
|
||||
title: "SystemDataUsageResponse"
|
||||
properties:
|
||||
LayersSize:
|
||||
type: "integer"
|
||||
@@ -7413,12 +7379,7 @@ paths:
|
||||
description: "No error"
|
||||
schema:
|
||||
type: "object"
|
||||
title: "ExecInspectResponse"
|
||||
properties:
|
||||
CanRemove:
|
||||
type: "boolean"
|
||||
DetachKeys:
|
||||
type: "string"
|
||||
ID:
|
||||
type: "string"
|
||||
Running:
|
||||
@@ -7484,7 +7445,6 @@ paths:
|
||||
description: "Summary volume data that matches the query"
|
||||
schema:
|
||||
type: "object"
|
||||
title: "VolumeListResponse"
|
||||
required: [Volumes, Warnings]
|
||||
properties:
|
||||
Volumes:
|
||||
@@ -7666,7 +7626,6 @@ paths:
|
||||
description: "No error"
|
||||
schema:
|
||||
type: "object"
|
||||
title: "VolumePruneResponse"
|
||||
properties:
|
||||
VolumesDeleted:
|
||||
description: "Volumes that were deleted"
|
||||
@@ -7845,7 +7804,6 @@ paths:
|
||||
description: "No error"
|
||||
schema:
|
||||
type: "object"
|
||||
title: "NetworkCreateResponse"
|
||||
properties:
|
||||
Id:
|
||||
description: "The ID of the created network."
|
||||
@@ -8048,7 +8006,6 @@ paths:
|
||||
description: "No error"
|
||||
schema:
|
||||
type: "object"
|
||||
title: "NetworkPruneResponse"
|
||||
properties:
|
||||
NetworksDeleted:
|
||||
description: "Networks that were deleted"
|
||||
@@ -8100,7 +8057,6 @@ paths:
|
||||
items:
|
||||
description: "Describes a permission the user has to accept upon installing the plugin."
|
||||
type: "object"
|
||||
title: "PluginPrivilegeItem"
|
||||
properties:
|
||||
Name:
|
||||
type: "string"
|
||||
@@ -8803,7 +8759,6 @@ paths:
|
||||
description: "no error"
|
||||
schema:
|
||||
type: "object"
|
||||
title: "UnlockKeyResponse"
|
||||
properties:
|
||||
UnlockKey:
|
||||
description: "The swarm's unlock key."
|
||||
@@ -8895,7 +8850,6 @@ paths:
|
||||
description: "no error"
|
||||
schema:
|
||||
type: "object"
|
||||
title: "ServiceCreateResponse"
|
||||
properties:
|
||||
ID:
|
||||
description: "The ID of the created service."
|
||||
@@ -9567,7 +9521,13 @@ paths:
|
||||
201:
|
||||
description: "no error"
|
||||
schema:
|
||||
$ref: "#/definitions/IdResponse"
|
||||
type: "object"
|
||||
properties:
|
||||
ID:
|
||||
description: "The ID of the created secret."
|
||||
type: "string"
|
||||
example:
|
||||
ID: "ktnbjxoalbkvbvedmg1urrz8h"
|
||||
409:
|
||||
description: "name conflicts with an existing object"
|
||||
schema:
|
||||
@@ -9766,7 +9726,13 @@ paths:
|
||||
201:
|
||||
description: "no error"
|
||||
schema:
|
||||
$ref: "#/definitions/IdResponse"
|
||||
type: "object"
|
||||
properties:
|
||||
ID:
|
||||
description: "The ID of the created config."
|
||||
type: "string"
|
||||
example:
|
||||
ID: "ktnbjxoalbkvbvedmg1urrz8h"
|
||||
409:
|
||||
description: "name conflicts with an existing object"
|
||||
schema:
|
||||
@@ -9912,7 +9878,6 @@ paths:
|
||||
schema:
|
||||
type: "object"
|
||||
x-go-name: DistributionInspect
|
||||
title: "DistributionInspectResponse"
|
||||
required: [Descriptor, Platforms]
|
||||
properties:
|
||||
Descriptor:
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
|
||||
|
||||
{{ range .ExtraSchemas }}
|
||||
// {{ .Name }} {{ comment .Description }}
|
||||
// {{ .Name }} {{ template "docstring" . }}
|
||||
// swagger:model {{ .Name }}
|
||||
{{ template "schema" . }}
|
||||
{{ end }}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user