Skip to content

Commit

Permalink
Confirm a push to public Docker registry
Browse files Browse the repository at this point in the history
Ask the user to confirm a push of image to public Docker registry. The
confirmation can be suppressed with the use of `-f, --force` flag.

Added --confirm-def-push boolean flag to Docker daemon. It causes
client to ask for confirmation before a push to public registry. It
defaults to true. If set to false, push won't be restricted in any
way unless the registry in question is blocked.

No confirmation is needed for push to unofficial registry.

Signed-off-by: Michal Minar <miminar@redhat.com>
  • Loading branch information
Michal Minar committed May 18, 2015
1 parent c12e24b commit e73a596
Show file tree
Hide file tree
Showing 12 changed files with 338 additions and 11 deletions.
39 changes: 38 additions & 1 deletion api/client/push.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,38 @@
package client

import (
"fmt"
"net/url"
"strings"

flag "github.com/docker/docker/pkg/mflag"
"github.com/docker/docker/pkg/parsers"
"github.com/docker/docker/registry"
)

func (cli *DockerCli) confirmPush() bool {
const prompt = "Do you really want to push to public registry? [y/n]: "
answer := ""
fmt.Fprintln(cli.out, "")

for answer != "n" && answer != "y" {
fmt.Fprint(cli.out, prompt)
answer = strings.ToLower(strings.TrimSpace(readInput(cli.in, cli.out)))
}

if answer == "n" {
fmt.Fprintln(cli.out, "Nothing pushed.")
}

return answer == "y"
}

// CmdPush pushes an image or repository to the registry.
//
// Usage: docker push NAME[:TAG]
func (cli *DockerCli) CmdPush(args ...string) error {
cmd := cli.Subcmd("push", "NAME[:TAG]", "Push an image or a repository to the registry", true)
force := cmd.Bool([]string{"f", "-force"}, false, "Push to public registry without confirmation")
cmd.Require(flag.Exact, 1)

cmd.ParseFlags(args, true)
Expand All @@ -29,7 +49,24 @@ func (cli *DockerCli) CmdPush(args ...string) error {

v := url.Values{}
v.Set("tag", tag)
if *force {
v.Set("force", "1")
}

_, _, err = cli.clientRequestAttemptLogin("POST", "/images/"+remote+"/push?"+v.Encode(), nil, cli.out, repoInfo.Index, "push")
push := func() error {
_, _, err = cli.clientRequestAttemptLogin("POST", "/images/"+remote+"/push?"+v.Encode(), nil, cli.out, repoInfo.Index, "push")
return err
}
if err = push(); err != nil {
if v.Get("force") != "1" && strings.Contains(err.Error(), "Status 403") {
if !cli.confirmPush() {
return nil
}
v.Set("force", "1")
if err = push(); err == nil {
return nil
}
}
}
return err
}
11 changes: 11 additions & 0 deletions api/client/utils.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package client

import (
"bufio"
"bytes"
"encoding/base64"
"encoding/json"
Expand Down Expand Up @@ -343,3 +344,13 @@ func readBody(stream io.ReadCloser, statusCode int, err error) ([]byte, int, err
}
return body, statusCode, nil
}

func readInput(in io.Reader, out io.Writer) string {
reader := bufio.NewReader(in)
line, _, err := reader.ReadLine()
if err != nil {
fmt.Fprintln(out, err.Error())
os.Exit(1)
}
return string(line)
}
2 changes: 2 additions & 0 deletions api/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ func httpError(w http.ResponseWriter, err error) {
"impossible": http.StatusNotAcceptable,
"wrong login/password": http.StatusUnauthorized,
"hasn't been activated": http.StatusForbidden,
"needs to be forced": http.StatusForbidden,
} {
if strings.Contains(errStr, keyword) {
statusCode = status
Expand Down Expand Up @@ -811,6 +812,7 @@ func (s *Server) postImagesPush(version version.Version, w http.ResponseWriter,
MetaHeaders: metaHeaders,
AuthConfig: authConfig,
Tag: r.Form.Get("tag"),
Force: boolValue(r, "force"),
OutStream: output,
}

Expand Down
2 changes: 2 additions & 0 deletions contrib/completion/fish/docker.fish
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ complete -c docker -f -n '__fish_docker_no_subcommand' -l api-cors-header -d "Se
complete -c docker -f -n '__fish_docker_no_subcommand' -s b -l bridge -d 'Attach containers to a pre-existing network bridge'
complete -c docker -f -n '__fish_docker_no_subcommand' -l bip -d "Use this CIDR notation address for the network bridge's IP, not compatible with -b"
complete -c docker -f -n '__fish_docker_no_subcommand' -l block-registry -d "Don't contact given registry"
complete -c docker -f -n '__fish_docker_no_subcommand' -l confirm-def-push -d 'Confirm a push to default registry'
complete -c docker -f -n '__fish_docker_no_subcommand' -s D -l debug -d 'Enable debug mode'
complete -c docker -f -n '__fish_docker_no_subcommand' -s d -l daemon -d 'Enable daemon mode'
complete -c docker -f -n '__fish_docker_no_subcommand' -l dns -d 'Force Docker to use specific DNS servers'
Expand Down Expand Up @@ -270,6 +271,7 @@ complete -c docker -A -f -n '__fish_seen_subcommand_from pull' -a '(__fish_print

# push
complete -c docker -f -n '__fish_docker_no_subcommand' -a push -d 'Push an image or a repository to a Docker registry server'
complete -c docker -A -f -n '__fish_seen_subcommand_from push' -s f -l force -d 'Push an image or a repository to the registry'
complete -c docker -A -f -n '__fish_seen_subcommand_from push' -l help -d 'Print usage'
complete -c docker -A -f -n '__fish_seen_subcommand_from push' -a '(__fish_print_docker_images)' -d "Image"
complete -c docker -A -f -n '__fish_seen_subcommand_from push' -a '(__fish_print_docker_repositories)' -d "Repository"
Expand Down
2 changes: 2 additions & 0 deletions daemon/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type CommonConfig struct {
TrustKeyPath string
BlockedRegistries opts.ListOpts
AdditionalRegistries opts.ListOpts
ConfirmDefPush bool
}

// InstallCommonFlags adds command-line options to the top-level flag parser for
Expand Down Expand Up @@ -76,6 +77,7 @@ func (config *Config) InstallCommonFlags() {
flag.Var(&config.BlockedRegistries, []string{"-block-registry"}, "Don't contact given registry")
config.AdditionalRegistries = opts.NewListOpts(registry.ValidateIndexName)
flag.Var(&config.AdditionalRegistries, []string{"-add-registry"}, "Registry to query before a public one")
flag.BoolVar(&config.ConfirmDefPush, []string{"-confirm-def-push"}, true, "Confirm a push to default registry")
}

func getDefaultNetworkMtu() int {
Expand Down
11 changes: 6 additions & 5 deletions daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -865,11 +865,12 @@ func NewDaemon(config *Config, registryService *registry.Service) (daemon *Daemo
eventsService := events.New()
logrus.Debug("Creating repository list")
tagCfg := &graph.TagStoreConfig{
Graph: g,
Key: trustKey,
Registry: registryService,
Events: eventsService,
Trust: trustService,
Graph: g,
Key: trustKey,
Registry: registryService,
Events: eventsService,
Trust: trustService,
ConfirmDefPush: config.ConfirmDefPush,
}
repositories, err := graph.NewTagStore(path.Join(config.Root, "repositories-"+d.driver.String()), tagCfg)
if err != nil {
Expand Down
7 changes: 7 additions & 0 deletions docs/man/docker-push.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ docker-push - Push an image or a repository to a registry

# SYNOPSIS
**docker push**
[**-f**|**--force**[=*false*]]
[**--help**]
NAME[:TAG] | [REGISTRY_HOST[:REGISTRY_PORT]/]NAME[:TAG]

Expand All @@ -15,7 +16,13 @@ This command pushes an image or a repository to a registry. If you do not
specify a `REGISTRY_HOST`, the command uses Docker's public registry located at
`registry-1.docker.io` by default.

If an image is about to be pushed to Docker registry without *force* flag
supplied, Docker will ask for confirmation.

# OPTIONS
**-f**, **--force**=*true*|*false*
Force a push to public registry

**--help**
Print usage statement

Expand Down
3 changes: 3 additions & 0 deletions docs/man/docker.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ To see the man page for a command run **man docker <command>**.
**--block-registry**=[]
**EXPERIMENTAL** Prevent Docker daemon from contacting specified registries. There are two special keywords recognized. The first is "public" and represents public Docker registry. The second is "all" which causes all registries but those added with **--add-registry** flag to be blocked.

**--confirm-def-push**=*true*|*false*
Makes Docker ask for a confirmation before a push to public registry. Default is true.

**-D**, **--debug**=*true*|*false*
Enable debug mode. Default is false.

Expand Down
2 changes: 2 additions & 0 deletions docs/sources/reference/api/docker_remote_api_v1.18.md
Original file line number Diff line number Diff line change
Expand Up @@ -1353,6 +1353,7 @@ Return low-level information on the image `name`
Status Codes:

- **200** – no error
- **403** - refused to push to public registry
- **404** – no such image
- **500** – server error

Expand Down Expand Up @@ -1421,6 +1422,7 @@ Push the image `name` on the registry

Query Parameters:

- **force** - force push to public Docker registry
- **tag** – the tag to associate with the image on the registry, optional

Request Headers:
Expand Down
7 changes: 7 additions & 0 deletions graph/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type ImagePushConfig struct {
MetaHeaders map[string][]string
AuthConfig *cliconfig.AuthConfig
Tag string
Force bool
OutStream io.Writer
}

Expand Down Expand Up @@ -523,6 +524,12 @@ func (s *TagStore) Push(localName string, imagePushConfig *ImagePushConfig) erro
return fmt.Errorf("You cannot push a \"root\" repository. Please rename your repository to <user>/<repo> (ex: %s/%s)", username, name)
}

if repoInfo.Index.Official && s.ConfirmDefPush && !imagePushConfig.Force {
return fmt.Errorf("Error: Status 403 trying to push repository %s to official registry: needs to be forced", localName)
} else if repoInfo.Index.Official && !s.ConfirmDefPush && imagePushConfig.Force {
logrus.Infof("Push of %s to official registry has been forced", localName)
}

if _, err := s.poolAdd("push", repoInfo.LocalName); err != nil {
return err
}
Expand Down
13 changes: 8 additions & 5 deletions graph/tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type TagStore struct {
registryService *registry.Service
eventsService *events.Events
trustService *trust.TrustStore
ConfirmDefPush bool
}

type Repository map[string]string
Expand All @@ -67,11 +68,12 @@ func (r Repository) Contains(u Repository) bool {
}

type TagStoreConfig struct {
Graph *Graph
Key libtrust.PrivateKey
Registry *registry.Service
Events *events.Events
Trust *trust.TrustStore
Graph *Graph
Key libtrust.PrivateKey
Registry *registry.Service
Events *events.Events
Trust *trust.TrustStore
ConfirmDefPush bool
}

func NewTagStore(path string, cfg *TagStoreConfig) (*TagStore, error) {
Expand All @@ -90,6 +92,7 @@ func NewTagStore(path string, cfg *TagStoreConfig) (*TagStore, error) {
registryService: cfg.Registry,
eventsService: cfg.Events,
trustService: cfg.Trust,
ConfirmDefPush: cfg.ConfirmDefPush,
}
// Load the json file if it exists, otherwise create it.
if err := store.reload(); os.IsNotExist(err) {
Expand Down

0 comments on commit e73a596

Please sign in to comment.