Merge pull request #23724 from tiborvass/cherry-picks-for-1.12.0-rc3

[WIP] Cherry picks for 1.12.0 rc3
This commit is contained in:
Tibor Vass 2016-07-01 03:59:35 -07:00 committed by GitHub
commit 39bc769113
311 changed files with 8273 additions and 3645 deletions

View file

@ -217,7 +217,7 @@ RUN ./contrib/download-frozen-image-v2.sh /docker-frozen-images \
# Download man page generator
RUN set -x \
&& export GOPATH="$(mktemp -d)" \
&& git clone --depth 1 -b v1.0.4 https://github.com/cpuguy83/go-md2man.git "$GOPATH/src/github.com/cpuguy83/go-md2man" \
&& git clone --depth 1 -b v1.0.5 https://github.com/cpuguy83/go-md2man.git "$GOPATH/src/github.com/cpuguy83/go-md2man" \
&& git clone --depth 1 -b v1.4 https://github.com/russross/blackfriday.git "$GOPATH/src/github.com/russross/blackfriday" \
&& go get -v -d github.com/cpuguy83/go-md2man \
&& go build -v -o /usr/local/bin/go-md2man github.com/cpuguy83/go-md2man \
@ -244,7 +244,7 @@ RUN set -x \
&& rm -rf "$GOPATH"
# Install containerd
ENV CONTAINERD_COMMIT 860f3a94940894ac0a106eff4bd1616a67407ee2
ENV CONTAINERD_COMMIT b93a33be39bc4ef0fb00bfcb79147a28c33d9d43
RUN set -x \
&& export GOPATH="$(mktemp -d)" \
&& git clone https://github.com/docker/containerd.git "$GOPATH/src/github.com/docker/containerd" \

View file

@ -164,7 +164,7 @@ RUN ./contrib/download-frozen-image-v2.sh /docker-frozen-images \
# Download man page generator
RUN set -x \
&& export GOPATH="$(mktemp -d)" \
&& git clone --depth 1 -b v1.0.4 https://github.com/cpuguy83/go-md2man.git "$GOPATH/src/github.com/cpuguy83/go-md2man" \
&& git clone --depth 1 -b v1.0.5 https://github.com/cpuguy83/go-md2man.git "$GOPATH/src/github.com/cpuguy83/go-md2man" \
&& git clone --depth 1 -b v1.4 https://github.com/russross/blackfriday.git "$GOPATH/src/github.com/russross/blackfriday" \
&& go get -v -d github.com/cpuguy83/go-md2man \
&& go build -v -o /usr/local/bin/go-md2man github.com/cpuguy83/go-md2man \
@ -191,7 +191,7 @@ RUN set -x \
&& rm -rf "$GOPATH"
# Install containerd
ENV CONTAINERD_COMMIT 860f3a94940894ac0a106eff4bd1616a67407ee2
ENV CONTAINERD_COMMIT b93a33be39bc4ef0fb00bfcb79147a28c33d9d43
RUN set -x \
&& export GOPATH="$(mktemp -d)" \
&& git clone https://github.com/docker/containerd.git "$GOPATH/src/github.com/docker/containerd" \

View file

@ -173,7 +173,7 @@ RUN ./contrib/download-frozen-image-v2.sh /docker-frozen-images \
# Download man page generator
RUN set -x \
&& export GOPATH="$(mktemp -d)" \
&& git clone --depth 1 -b v1.0.4 https://github.com/cpuguy83/go-md2man.git "$GOPATH/src/github.com/cpuguy83/go-md2man" \
&& git clone --depth 1 -b v1.0.5 https://github.com/cpuguy83/go-md2man.git "$GOPATH/src/github.com/cpuguy83/go-md2man" \
&& git clone --depth 1 -b v1.4 https://github.com/russross/blackfriday.git "$GOPATH/src/github.com/russross/blackfriday" \
&& go get -v -d github.com/cpuguy83/go-md2man \
&& go build -v -o /usr/local/bin/go-md2man github.com/cpuguy83/go-md2man \
@ -200,7 +200,7 @@ RUN set -x \
&& rm -rf "$GOPATH"
# Install containerd
ENV CONTAINERD_COMMIT 860f3a94940894ac0a106eff4bd1616a67407ee2
ENV CONTAINERD_COMMIT b93a33be39bc4ef0fb00bfcb79147a28c33d9d43
RUN set -x \
&& export GOPATH="$(mktemp -d)" \
&& git clone https://github.com/docker/containerd.git "$GOPATH/src/github.com/docker/containerd" \

View file

@ -85,7 +85,7 @@ RUN set -x \
&& rm -rf "$GOPATH"
# Install containerd
ENV CONTAINERD_COMMIT 860f3a94940894ac0a106eff4bd1616a67407ee2
ENV CONTAINERD_COMMIT b93a33be39bc4ef0fb00bfcb79147a28c33d9d43
RUN set -x \
&& export GOPATH="$(mktemp -d)" \
&& git clone https://github.com/docker/containerd.git "$GOPATH/src/github.com/docker/containerd" \

View file

@ -188,7 +188,7 @@ RUN ./contrib/download-frozen-image-v2.sh /docker-frozen-images \
# Download man page generator
RUN set -x \
&& export GOPATH="$(mktemp -d)" \
&& git clone --depth 1 -b v1.0.4 https://github.com/cpuguy83/go-md2man.git "$GOPATH/src/github.com/cpuguy83/go-md2man" \
&& git clone --depth 1 -b v1.0.5 https://github.com/cpuguy83/go-md2man.git "$GOPATH/src/github.com/cpuguy83/go-md2man" \
&& git clone --depth 1 -b v1.4 https://github.com/russross/blackfriday.git "$GOPATH/src/github.com/russross/blackfriday" \
&& go get -v -d github.com/cpuguy83/go-md2man \
&& go build -v -o /usr/local/bin/go-md2man github.com/cpuguy83/go-md2man \
@ -215,7 +215,7 @@ RUN set -x \
&& rm -rf "$GOPATH"
# Install containerd
ENV CONTAINERD_COMMIT 860f3a94940894ac0a106eff4bd1616a67407ee2
ENV CONTAINERD_COMMIT b93a33be39bc4ef0fb00bfcb79147a28c33d9d43
RUN set -x \
&& export GOPATH="$(mktemp -d)" \
&& git clone https://github.com/docker/containerd.git "$GOPATH/src/github.com/docker/containerd" \

View file

@ -181,7 +181,7 @@ RUN ./contrib/download-frozen-image-v2.sh /docker-frozen-images \
# Download man page generator
RUN set -x \
&& export GOPATH="$(mktemp -d)" \
&& git clone --depth 1 -b v1.0.4 https://github.com/cpuguy83/go-md2man.git "$GOPATH/src/github.com/cpuguy83/go-md2man" \
&& git clone --depth 1 -b v1.0.5 https://github.com/cpuguy83/go-md2man.git "$GOPATH/src/github.com/cpuguy83/go-md2man" \
&& git clone --depth 1 -b v1.4 https://github.com/russross/blackfriday.git "$GOPATH/src/github.com/russross/blackfriday" \
&& go get -v -d github.com/cpuguy83/go-md2man \
&& go build -v -o /usr/local/bin/go-md2man github.com/cpuguy83/go-md2man \
@ -208,7 +208,7 @@ RUN set -x \
&& rm -rf "$GOPATH"
# Install containerd
ENV CONTAINERD_COMMIT 860f3a94940894ac0a106eff4bd1616a67407ee2
ENV CONTAINERD_COMMIT b93a33be39bc4ef0fb00bfcb79147a28c33d9d43
RUN set -x \
&& export GOPATH="$(mktemp -d)" \
&& git clone https://github.com/docker/containerd.git "$GOPATH/src/github.com/docker/containerd" \

View file

@ -68,7 +68,7 @@ RUN set -x \
&& rm -rf "$GOPATH"
# Install containerd
ENV CONTAINERD_COMMIT 860f3a94940894ac0a106eff4bd1616a67407ee2
ENV CONTAINERD_COMMIT b93a33be39bc4ef0fb00bfcb79147a28c33d9d43
RUN set -x \
&& export GOPATH="$(mktemp -d)" \
&& git clone https://github.com/docker/containerd.git "$GOPATH/src/github.com/docker/containerd" \

View file

@ -15,7 +15,7 @@ type diffOptions struct {
container string
}
// NewDiffCommand creats a new cobra.Command for `docker diff`
// NewDiffCommand creates a new cobra.Command for `docker diff`
func NewDiffCommand(dockerCli *client.DockerCli) *cobra.Command {
var opts diffOptions

View file

@ -18,7 +18,7 @@ type restartOptions struct {
containers []string
}
// NewRestartCommand creats a new cobra.Command for `docker restart`
// NewRestartCommand creates a new cobra.Command for `docker restart`
func NewRestartCommand(dockerCli *client.DockerCli) *cobra.Command {
var opts restartOptions

View file

@ -20,7 +20,7 @@ type rmOptions struct {
containers []string
}
// NewRmCommand creats a new cobra.Command for `docker rm`
// NewRmCommand creates a new cobra.Command for `docker rm`
func NewRmCommand(dockerCli *client.DockerCli) *cobra.Command {
var opts rmOptions

View file

@ -63,8 +63,9 @@ func runStart(dockerCli *client.DockerCli, opts *startOptions) error {
return err
}
// We always use c.ID instead of container to maintain consistency during `docker start`
if !c.Config.Tty {
sigc := dockerCli.ForwardAllSignals(ctx, container)
sigc := dockerCli.ForwardAllSignals(ctx, c.ID)
defer signal.StopCatch(sigc)
}
@ -86,7 +87,7 @@ func runStart(dockerCli *client.DockerCli, opts *startOptions) error {
in = dockerCli.In()
}
resp, errAttach := dockerCli.Client().ContainerAttach(ctx, container, options)
resp, errAttach := dockerCli.Client().ContainerAttach(ctx, c.ID, options)
if errAttach != nil && errAttach != httputil.ErrPersistEOF {
// ContainerAttach return an ErrPersistEOF (connection closed)
// means server met an error and put it in Hijacked connection
@ -103,7 +104,7 @@ func runStart(dockerCli *client.DockerCli, opts *startOptions) error {
})
// 3. Start the container.
if err := dockerCli.Client().ContainerStart(ctx, container, types.ContainerStartOptions{}); err != nil {
if err := dockerCli.Client().ContainerStart(ctx, c.ID, types.ContainerStartOptions{}); err != nil {
cancelFun()
<-cErr
return err
@ -111,14 +112,14 @@ func runStart(dockerCli *client.DockerCli, opts *startOptions) error {
// 4. Wait for attachment to break.
if c.Config.Tty && dockerCli.IsTerminalOut() {
if err := dockerCli.MonitorTtySize(ctx, container, false); err != nil {
if err := dockerCli.MonitorTtySize(ctx, c.ID, false); err != nil {
fmt.Fprintf(dockerCli.Err(), "Error monitoring TTY size: %s\n", err)
}
}
if attchErr := <-cErr; attchErr != nil {
return attchErr
}
_, status, err := getExitCode(dockerCli, ctx, container)
_, status, err := getExitCode(dockerCli, ctx, c.ID)
if err != nil {
return err
}

View file

@ -18,7 +18,7 @@ type topOptions struct {
args []string
}
// NewTopCommand creats a new cobra.Command for `docker top`
// NewTopCommand creates a new cobra.Command for `docker top`
func NewTopCommand(dockerCli *client.DockerCli) *cobra.Command {
var opts topOptions

View file

@ -15,7 +15,7 @@ type waitOptions struct {
containers []string
}
// NewWaitCommand creats a new cobra.Command for `docker wait`
// NewWaitCommand creates a new cobra.Command for `docker wait`
func NewWaitCommand(dockerCli *client.DockerCli) *cobra.Command {
var opts waitOptions

View file

@ -28,7 +28,7 @@ func New(client client.APIClient, noResolve bool) *IDResolver {
func (r *IDResolver) get(ctx context.Context, t interface{}, id string) (string, error) {
switch t.(type) {
case swarm.Node:
node, err := r.client.NodeInspect(ctx, id)
node, _, err := r.client.NodeInspectWithRaw(ctx, id)
if err != nil {
return id, nil
}

View file

@ -67,7 +67,7 @@ func NewBuildCommand(dockerCli *client.DockerCli) *cobra.Command {
}
cmd := &cobra.Command{
Use: "build PATH | URL | -",
Use: "build [OPTIONS] PATH | URL | -",
Short: "Build an image from a Dockerfile",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {

View file

@ -14,7 +14,7 @@ import (
// NewPushCommand creates a new `docker push` command
func NewPushCommand(dockerCli *client.DockerCli) *cobra.Command {
cmd := &cobra.Command{
Use: "push NAME[:TAG]",
Use: "push [OPTIONS] NAME[:TAG]",
Short: "Push an image or a repository to a registry",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {

View file

@ -41,7 +41,7 @@ func (cli *DockerCli) CmdInfo(args ...string) error {
// print a warning if devicemapper is using a loopback file
if pair[0] == "Data loop file" {
fmt.Fprintln(cli.err, " WARNING: Usage of loopback devices is strongly discouraged for production use. Either use `--storage-opt dm.thinpooldev` or use `--storage-opt dm.no_warn_on_loop_devices=true` to suppress this warning.")
fmt.Fprintln(cli.err, " WARNING: Usage of loopback devices is strongly discouraged for production use. Use `--storage-opt dm.thinpooldev` to specify a custom block storage device.")
}
}

View file

@ -86,10 +86,10 @@ func (cli *DockerCli) inspectAll(ctx context.Context, getSize bool) inspect.GetR
}
return nil, nil, err
}
return i, rawImage, err
return i, rawImage, nil
}
return nil, nil, err
}
return c, rawContainer, err
return c, rawContainer, nil
}
}

View file

@ -28,7 +28,7 @@ func newConnectCommand(dockerCli *client.DockerCli) *cobra.Command {
cmd := &cobra.Command{
Use: "connect [OPTIONS] NETWORK CONTAINER",
Short: "Connects a container to a network",
Short: "Connect a container to a network",
Args: cli.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
opts.network = args[0]

View file

@ -40,7 +40,7 @@ func newCreateCommand(dockerCli *client.DockerCli) *cobra.Command {
}
cmd := &cobra.Command{
Use: "create",
Use: "create [OPTIONS] NETWORK",
Short: "Create a network",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {

View file

@ -19,7 +19,7 @@ func newDisconnectCommand(dockerCli *client.DockerCli) *cobra.Command {
cmd := &cobra.Command{
Use: "disconnect [OPTIONS] NETWORK CONTAINER",
Short: "Disconnects container from a network",
Short: "Disconnect a container from a network",
Args: cli.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
opts.network = args[0]

View file

@ -19,7 +19,7 @@ func newInspectCommand(dockerCli *client.DockerCli) *cobra.Command {
cmd := &cobra.Command{
Use: "inspect [OPTIONS] NETWORK [NETWORK...]",
Short: "Displays detailed information on one or more networks",
Short: "Display detailed information on one or more networks",
Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.names = args

View file

@ -7,34 +7,25 @@ import (
"github.com/docker/docker/cli"
"github.com/docker/engine-api/types/swarm"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
func newAcceptCommand(dockerCli *client.DockerCli) *cobra.Command {
var flags *pflag.FlagSet
cmd := &cobra.Command{
return &cobra.Command{
Use: "accept NODE [NODE...]",
Short: "Accept a node in the swarm",
Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runAccept(dockerCli, flags, args)
return runAccept(dockerCli, args)
},
}
flags = cmd.Flags()
return cmd
}
func runAccept(dockerCli *client.DockerCli, flags *pflag.FlagSet, args []string) error {
for _, id := range args {
if err := runUpdate(dockerCli, id, func(node *swarm.Node) {
node.Spec.Membership = swarm.NodeMembershipAccepted
}); err != nil {
return err
}
fmt.Fprintf(dockerCli.Out(), "Node %s accepted in the swarm.\n", id)
func runAccept(dockerCli *client.DockerCli, nodes []string) error {
accept := func(node *swarm.Node) {
node.Spec.Membership = swarm.NodeMembershipAccepted
}
return nil
success := func(nodeID string) {
fmt.Fprintf(dockerCli.Out(), "Node %s accepted in the swarm.\n", nodeID)
}
return updateNodes(dockerCli, nodes, accept, success)
}

View file

@ -7,34 +7,25 @@ import (
"github.com/docker/docker/cli"
"github.com/docker/engine-api/types/swarm"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
func newDemoteCommand(dockerCli *client.DockerCli) *cobra.Command {
var flags *pflag.FlagSet
cmd := &cobra.Command{
return &cobra.Command{
Use: "demote NODE [NODE...]",
Short: "Demote a node from manager in the swarm",
Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runDemote(dockerCli, flags, args)
return runDemote(dockerCli, args)
},
}
flags = cmd.Flags()
return cmd
}
func runDemote(dockerCli *client.DockerCli, flags *pflag.FlagSet, args []string) error {
for _, id := range args {
if err := runUpdate(dockerCli, id, func(node *swarm.Node) {
node.Spec.Role = swarm.NodeRoleWorker
}); err != nil {
return err
}
fmt.Fprintf(dockerCli.Out(), "Manager %s demoted in the swarm.\n", id)
func runDemote(dockerCli *client.DockerCli, nodes []string) error {
demote := func(node *swarm.Node) {
node.Spec.Role = swarm.NodeRoleWorker
}
return nil
success := func(nodeID string) {
fmt.Fprintf(dockerCli.Out(), "Manager %s demoted in the swarm.\n", nodeID)
}
return updateNodes(dockerCli, nodes, demote, success)
}

View file

@ -27,7 +27,7 @@ func newInspectCommand(dockerCli *client.DockerCli) *cobra.Command {
cmd := &cobra.Command{
Use: "inspect [OPTIONS] self|NODE [NODE...]",
Short: "Inspect a node in the swarm",
Short: "Display detailed information on one or more nodes",
Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.nodeIds = args
@ -49,7 +49,7 @@ func runInspect(dockerCli *client.DockerCli, opts inspectOptions) error {
if err != nil {
return nil, nil, err
}
node, err := client.NodeInspect(ctx, nodeRef)
node, _, err := client.NodeInspectWithRaw(ctx, nodeRef)
return node, nil, err
}
@ -96,7 +96,7 @@ func printNode(out io.Writer, node swarm.Node) {
if node.ManagerStatus != nil {
fmt.Fprintln(out, "Manager Status:")
fmt.Fprintf(out, " Address:\t\t%s\n", node.ManagerStatus.Addr)
fmt.Fprintf(out, " Raft status:\t\t%s\n", client.PrettyPrint(node.ManagerStatus.Reachability))
fmt.Fprintf(out, " Raft Status:\t\t%s\n", client.PrettyPrint(node.ManagerStatus.Reachability))
leader := "No"
if node.ManagerStatus.Leader {
leader = "Yes"

View file

@ -74,16 +74,12 @@ func printTable(out io.Writer, nodes []swarm.Node, info types.Info) {
// Ignore flushing errors
defer writer.Flush()
fmt.Fprintf(writer, listItemFmt, "ID", "NAME", "MEMBERSHIP", "STATUS", "AVAILABILITY", "MANAGER STATUS")
fmt.Fprintf(writer, listItemFmt, "ID", "HOSTNAME", "MEMBERSHIP", "STATUS", "AVAILABILITY", "MANAGER STATUS")
for _, node := range nodes {
name := node.Spec.Name
name := node.Description.Hostname
availability := string(node.Spec.Availability)
membership := string(node.Spec.Membership)
if name == "" {
name = node.Description.Hostname
}
reachability := ""
if node.ManagerStatus != nil {
if node.ManagerStatus.Leader {

View file

@ -7,34 +7,25 @@ import (
"github.com/docker/docker/cli"
"github.com/docker/engine-api/types/swarm"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
func newPromoteCommand(dockerCli *client.DockerCli) *cobra.Command {
var flags *pflag.FlagSet
cmd := &cobra.Command{
return &cobra.Command{
Use: "promote NODE [NODE...]",
Short: "Promote a node to a manager in the swarm",
Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runPromote(dockerCli, flags, args)
return runPromote(dockerCli, args)
},
}
flags = cmd.Flags()
return cmd
}
func runPromote(dockerCli *client.DockerCli, flags *pflag.FlagSet, args []string) error {
for _, id := range args {
if err := runUpdate(dockerCli, id, func(node *swarm.Node) {
node.Spec.Role = swarm.NodeRoleManager
}); err != nil {
return err
}
fmt.Fprintf(dockerCli.Out(), "Node %s promoted to a manager in the swarm.\n", id)
func runPromote(dockerCli *client.DockerCli, nodes []string) error {
promote := func(node *swarm.Node) {
node.Spec.Role = swarm.NodeRoleManager
}
return nil
success := func(nodeID string) {
fmt.Fprintf(dockerCli.Out(), "Node %s promoted to a manager in the swarm.\n", nodeID)
}
return updateNodes(dockerCli, nodes, promote, success)
}

View file

@ -48,14 +48,14 @@ func runTasks(dockerCli *client.DockerCli, opts tasksOptions) error {
if err != nil {
return nil
}
node, err := client.NodeInspect(ctx, nodeRef)
node, _, err := client.NodeInspectWithRaw(ctx, nodeRef)
if err != nil {
return err
}
filter := opts.filter.Value()
filter.Add("node", node.ID)
if !opts.all {
if !opts.all && !filter.Include("desired_state") {
filter.Add("desired_state", string(swarm.TaskStateRunning))
filter.Add("desired_state", string(swarm.TaskStateAccepted))

View file

@ -5,7 +5,6 @@ import (
"github.com/docker/docker/api/client"
"github.com/docker/docker/cli"
runconfigopts "github.com/docker/docker/runconfig/opts"
"github.com/docker/engine-api/types/swarm"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@ -14,90 +13,71 @@ import (
func newUpdateCommand(dockerCli *client.DockerCli) *cobra.Command {
var opts nodeOptions
var flags *pflag.FlagSet
cmd := &cobra.Command{
Use: "update [OPTIONS] NODE",
Short: "Update a node",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if err := runUpdate(dockerCli, args[0], mergeNodeUpdate(flags)); err != nil {
return err
}
fmt.Fprintln(dockerCli.Out(), args[0])
return nil
return runUpdate(dockerCli, cmd.Flags(), args[0])
},
}
flags = cmd.Flags()
flags.StringVar(&opts.role, "role", "", "Role of the node (worker/manager)")
flags.StringVar(&opts.membership, "membership", "", "Membership of the node (accepted/rejected)")
flags.StringVar(&opts.availability, "availability", "", "Availability of the node (active/pause/drain)")
flags := cmd.Flags()
flags.StringVar(&opts.role, flagRole, "", "Role of the node (worker/manager)")
flags.StringVar(&opts.membership, flagMembership, "", "Membership of the node (accepted/rejected)")
flags.StringVar(&opts.availability, flagAvailability, "", "Availability of the node (active/pause/drain)")
return cmd
}
func runUpdate(dockerCli *client.DockerCli, nodeID string, mergeNode func(node *swarm.Node)) error {
func runUpdate(dockerCli *client.DockerCli, flags *pflag.FlagSet, nodeID string) error {
success := func(_ string) {
fmt.Fprintln(dockerCli.Out(), nodeID)
}
return updateNodes(dockerCli, []string{nodeID}, mergeNodeUpdate(flags), success)
}
func updateNodes(dockerCli *client.DockerCli, nodes []string, mergeNode func(node *swarm.Node), success func(nodeID string)) error {
client := dockerCli.Client()
ctx := context.Background()
node, err := client.NodeInspect(ctx, nodeID)
if err != nil {
return err
}
for _, nodeID := range nodes {
node, _, err := client.NodeInspectWithRaw(ctx, nodeID)
if err != nil {
return err
}
mergeNode(&node)
err = client.NodeUpdate(ctx, node.ID, node.Version, node.Spec)
if err != nil {
return err
mergeNode(&node)
err = client.NodeUpdate(ctx, node.ID, node.Version, node.Spec)
if err != nil {
return err
}
success(nodeID)
}
return nil
}
func mergeNodeUpdate(flags *pflag.FlagSet) func(*swarm.Node) {
return func(node *swarm.Node) {
mergeString := func(flag string, field *string) {
if flags.Changed(flag) {
*field, _ = flags.GetString(flag)
}
}
mergeRole := func(flag string, field *swarm.NodeRole) {
if flags.Changed(flag) {
str, _ := flags.GetString(flag)
*field = swarm.NodeRole(str)
}
}
mergeMembership := func(flag string, field *swarm.NodeMembership) {
if flags.Changed(flag) {
str, _ := flags.GetString(flag)
*field = swarm.NodeMembership(str)
}
}
mergeAvailability := func(flag string, field *swarm.NodeAvailability) {
if flags.Changed(flag) {
str, _ := flags.GetString(flag)
*field = swarm.NodeAvailability(str)
}
}
mergeLabels := func(flag string, field *map[string]string) {
if flags.Changed(flag) {
values, _ := flags.GetStringSlice(flag)
for key, value := range runconfigopts.ConvertKVStringsToMap(values) {
(*field)[key] = value
}
}
}
spec := &node.Spec
mergeString("name", &spec.Name)
// TODO: setting labels is not working
mergeLabels("label", &spec.Labels)
mergeRole("role", &spec.Role)
mergeMembership("membership", &spec.Membership)
mergeAvailability("availability", &spec.Availability)
if flags.Changed(flagRole) {
str, _ := flags.GetString(flagRole)
spec.Role = swarm.NodeRole(str)
}
if flags.Changed(flagMembership) {
str, _ := flags.GetString(flagMembership)
spec.Membership = swarm.NodeMembership(str)
}
if flags.Changed(flagAvailability) {
str, _ := flags.GetString(flagAvailability)
spec.Availability = swarm.NodeAvailability(str)
}
}
}
const (
flagRole = "role"
flagMembership = "membership"
flagAvailability = "availability"
)

View file

@ -57,6 +57,10 @@ func runInstall(dockerCli *client.DockerCli, opts pluginOptions) error {
ctx := context.Background()
repoInfo, err := registry.ParseRepositoryInfo(named)
if err != nil {
return err
}
authConfig := dockerCli.ResolveAuthConfig(ctx, repoInfo.Index)
encodedAuth, err := client.EncodeAuthToBase64(authConfig)

View file

@ -42,6 +42,9 @@ func runPush(dockerCli *client.DockerCli, name string) error {
ctx := context.Background()
repoInfo, err := registry.ParseRepositoryInfo(named)
if err != nil {
return err
}
authConfig := dockerCli.ResolveAuthConfig(ctx, repoInfo.Index)
encodedAuth, err := client.EncodeAuthToBase64(authConfig)

View file

@ -13,6 +13,7 @@ import (
"golang.org/x/net/context"
"github.com/docker/docker/pkg/term"
"github.com/docker/docker/reference"
"github.com/docker/docker/registry"
"github.com/docker/engine-api/types"
registrytypes "github.com/docker/engine-api/types/registry"
@ -42,7 +43,7 @@ func EncodeAuthToBase64(authConfig types.AuthConfig) (string, error) {
return base64.URLEncoding.EncodeToString(buf), nil
}
// RegistryAuthenticationPrivilegedFunc return a RequestPrivilegeFunc from the specified registry index info
// RegistryAuthenticationPrivilegedFunc returns a RequestPrivilegeFunc from the specified registry index info
// for the given command.
func (cli *DockerCli) RegistryAuthenticationPrivilegedFunc(index *registrytypes.IndexInfo, cmdName string) types.RequestPrivilegeFunc {
return func() (string, error) {
@ -103,14 +104,14 @@ func (cli *DockerCli) ConfigureAuth(flUser, flPassword, serverAddress string, is
// will hit this if you attempt docker login from mintty where stdin
// is a pipe, not a character based console.
if flPassword == "" && !cli.isTerminalIn {
return authconfig, fmt.Errorf("Error: Cannot perform an interactive logon from a non TTY device")
return authconfig, fmt.Errorf("Error: Cannot perform an interactive login from a non TTY device")
}
authconfig.Username = strings.TrimSpace(authconfig.Username)
if flUser = strings.TrimSpace(flUser); flUser == "" {
if isDefaultRegistry {
// if this is a defauly registry (docker hub), then display the following message.
// if this is a default registry (docker hub), then display the following message.
fmt.Fprintln(cli.out, "Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.")
}
cli.promptWithDefault("Username", authconfig.Username)
@ -148,6 +149,34 @@ func (cli *DockerCli) ConfigureAuth(flUser, flPassword, serverAddress string, is
return authconfig, nil
}
// resolveAuthConfigFromImage retrieves that AuthConfig using the image string
func (cli *DockerCli) resolveAuthConfigFromImage(ctx context.Context, image string) (types.AuthConfig, error) {
registryRef, err := reference.ParseNamed(image)
if err != nil {
return types.AuthConfig{}, err
}
repoInfo, err := registry.ParseRepositoryInfo(registryRef)
if err != nil {
return types.AuthConfig{}, err
}
authConfig := cli.ResolveAuthConfig(ctx, repoInfo.Index)
return authConfig, nil
}
// RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete image
func (cli *DockerCli) RetrieveAuthTokenFromImage(ctx context.Context, image string) (string, error) {
// Retrieve encoded auth token from the image reference
authConfig, err := cli.resolveAuthConfigFromImage(ctx, image)
if err != nil {
return "", err
}
encodedAuth, err := EncodeAuthToBase64(authConfig)
if err != nil {
return "", err
}
return encodedAuth, nil
}
func readInput(in io.Reader, out io.Writer) string {
reader := bufio.NewReader(in)
line, _, err := reader.ReadLine()

View file

@ -24,20 +24,35 @@ func newCreateCommand(dockerCli *client.DockerCli) *cobra.Command {
return runCreate(dockerCli, opts)
},
}
flags := cmd.Flags()
flags.StringVar(&opts.mode, flagMode, "replicated", "Service mode (replicated or global)")
addServiceFlags(cmd, opts)
cmd.Flags().SetInterspersed(false)
return cmd
}
func runCreate(dockerCli *client.DockerCli, opts *serviceOptions) error {
client := dockerCli.Client()
apiClient := dockerCli.Client()
headers := map[string][]string{}
service, err := opts.ToService()
if err != nil {
return err
}
response, err := client.ServiceCreate(context.Background(), service)
ctx := context.Background()
// only send auth if flag was set
if opts.registryAuth {
// Retrieve encoded auth token from the image reference
encodedAuth, err := dockerCli.RetrieveAuthTokenFromImage(ctx, opts.image)
if err != nil {
return err
}
headers["X-Registry-Auth"] = []string{encodedAuth}
}
response, err := apiClient.ServiceCreate(ctx, service, headers)
if err != nil {
return err
}

View file

@ -28,7 +28,7 @@ func newInspectCommand(dockerCli *client.DockerCli) *cobra.Command {
cmd := &cobra.Command{
Use: "inspect [OPTIONS] SERVICE [SERVICE...]",
Short: "Inspect a service",
Short: "Display detailed information on one or more services",
Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.refs = args

View file

@ -69,9 +69,20 @@ func runList(dockerCli *client.DockerCli, opts listOptions) error {
return err
}
nodes, err := client.NodeList(ctx, types.NodeListOptions{})
if err != nil {
return err
}
activeNodes := make(map[string]struct{})
for _, n := range nodes {
if n.Status.State == swarm.NodeStateReady {
activeNodes[n.ID] = struct{}{}
}
}
running := map[string]int{}
for _, task := range tasks {
if task.Status.State == "running" {
if _, nodeActive := activeNodes[task.NodeID]; nodeActive && task.Status.State == "running" {
running[task.ServiceID]++
}
}

View file

@ -160,6 +160,13 @@ func (m *MountOpt) Set(value string) error {
return mount.VolumeOptions
}
bindOptions := func() *swarm.BindOptions {
if mount.BindOptions == nil {
mount.BindOptions = new(swarm.BindOptions)
}
return mount.BindOptions
}
setValueOnMap := func(target map[string]string, value string) {
parts := strings.SplitN(value, "=", 2)
if len(parts) == 1 {
@ -194,7 +201,7 @@ func (m *MountOpt) Set(value string) error {
return fmt.Errorf("invalid value for writable: %s", value)
}
case "bind-propagation":
mount.BindOptions.Propagation = swarm.MountPropagation(strings.ToUpper(value))
bindOptions().Propagation = swarm.MountPropagation(strings.ToUpper(value))
case "volume-populate":
volumeOptions().Populate, err = strconv.ParseBool(value)
if err != nil {
@ -210,7 +217,7 @@ func (m *MountOpt) Set(value string) error {
}
setValueOnMap(volumeOptions().DriverConfig.Options, value)
default:
return fmt.Errorf("unexpected key '%s' in '%s'", key, value)
return fmt.Errorf("unexpected key '%s' in '%s'", key, field)
}
}
@ -310,7 +317,7 @@ func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec {
}
return &swarm.EndpointSpec{
Mode: swarm.ResolutionMode(e.mode),
Mode: swarm.ResolutionMode(strings.ToLower(e.mode)),
Ports: portConfigs,
}
}
@ -366,6 +373,8 @@ type serviceOptions struct {
update updateOptions
networks []string
endpoint endpointOptions
registryAuth bool
}
func newServiceOptions() *serviceOptions {
@ -429,7 +438,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
return service, nil
}
// addServiceFlags adds all flags that are common to both `create` and `update.
// addServiceFlags adds all flags that are common to both `create` and `update`.
// Any flags that are not common are added separately in the individual command
func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) {
flags := cmd.Flags()
@ -438,22 +447,21 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) {
flags.VarP(&opts.env, "env", "e", "Set environment variables")
flags.StringVarP(&opts.workdir, "workdir", "w", "", "Working directory inside the container")
flags.StringVarP(&opts.user, "user", "u", "", "Username or UID")
flags.StringVarP(&opts.user, flagUser, "u", "", "Username or UID")
flags.VarP(&opts.mounts, flagMount, "m", "Attach a mount to the service")
flags.Var(&opts.resources.limitCPU, flagLimitCPU, "Limit CPUs")
flags.Var(&opts.resources.limitMemBytes, flagLimitMemory, "Limit Memory")
flags.Var(&opts.resources.resCPU, flagReserveCPU, "Reserve CPUs")
flags.Var(&opts.resources.resMemBytes, flagReserveMemory, "Reserve Memory")
flags.Var(&opts.stopGrace, "stop-grace-period", "Time to wait before force killing a container")
flags.Var(&opts.stopGrace, flagStopGracePeriod, "Time to wait before force killing a container")
flags.StringVar(&opts.mode, flagMode, "replicated", "Service mode (replicated or global)")
flags.Var(&opts.replicas, flagReplicas, "Number of tasks")
flags.StringVar(&opts.restartPolicy.condition, flagRestartCondition, "", "Restart when condition is met (none, on_failure, or any)")
flags.Var(&opts.restartPolicy.delay, flagRestartDelay, "Delay between restart attempts")
flags.Var(&opts.restartPolicy.maxAttempts, flagRestartMaxAttempts, "Maximum number of restarts before giving up")
flags.Var(&opts.restartPolicy.window, flagRestartWindow, "Window used to evalulate the restart policy")
flags.Var(&opts.restartPolicy.window, flagRestartWindow, "Window used to evaluate the restart policy")
flags.StringSliceVar(&opts.constraints, flagConstraint, []string{}, "Placement constraints")
@ -461,28 +469,33 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) {
flags.DurationVar(&opts.update.delay, flagUpdateDelay, time.Duration(0), "Delay between updates")
flags.StringSliceVar(&opts.networks, flagNetwork, []string{}, "Network attachments")
flags.StringVar(&opts.endpoint.mode, flagEndpointMode, "", "Endpoint mode(Valid values: VIP, DNSRR)")
flags.StringVar(&opts.endpoint.mode, flagEndpointMode, "", "Endpoint mode(Valid values: vip, dnsrr)")
flags.VarP(&opts.endpoint.ports, flagPublish, "p", "Publish a port as a node port")
flags.BoolVar(&opts.registryAuth, flagRegistryAuth, false, "Send registry authentication details to Swarm agents")
}
const (
flagConstraint = "constraint"
flagName = "name"
flagEndpointMode = "endpoint-mode"
flagLabel = "label"
flagLimitCPU = "limit-cpu"
flagLimitMemory = "limit-memory"
flagMode = "mode"
flagMount = "mount"
flagName = "name"
flagNetwork = "network"
flagPublish = "publish"
flagReplicas = "replicas"
flagReserveCPU = "reserve-cpu"
flagReserveMemory = "reserve-memory"
flagMount = "mount"
flagMode = "mode"
flagReplicas = "replicas"
flagPublish = "publish"
flagNetwork = "network"
flagRestartCondition = "restart-condition"
flagRestartDelay = "restart-delay"
flagRestartMaxAttempts = "restart-max-attempts"
flagRestartWindow = "restart-window"
flagEndpointMode = "endpoint-mode"
flagUpdateParallelism = "update-parallelism"
flagStopGracePeriod = "stop-grace-period"
flagUpdateDelay = "update-delay"
flagUpdateParallelism = "update-parallelism"
flagUser = "user"
flagRegistryAuth = "registry-auth"
)

View file

@ -77,7 +77,7 @@ func runServiceScale(dockerCli *client.DockerCli, serviceID string, scale string
}
serviceMode.Replicated.Replicas = &uintScale
err = client.ServiceUpdate(ctx, service.ID, service.Version, service.Spec)
err = client.ServiceUpdate(ctx, service.ID, service.Version, service.Spec, nil)
if err != nil {
return err
}

View file

@ -18,18 +18,17 @@ import (
func newUpdateCommand(dockerCli *client.DockerCli) *cobra.Command {
opts := newServiceOptions()
var flags *pflag.FlagSet
cmd := &cobra.Command{
Use: "update [OPTIONS] SERVICE",
Short: "Update a service",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runUpdate(dockerCli, flags, args[0])
return runUpdate(dockerCli, cmd.Flags(), args[0])
},
}
flags = cmd.Flags()
flags := cmd.Flags()
flags.String("image", "", "Service image tag")
flags.StringSlice("command", []string{}, "Service command")
flags.StringSlice("arg", []string{}, "Service command args")
@ -38,19 +37,37 @@ func newUpdateCommand(dockerCli *client.DockerCli) *cobra.Command {
}
func runUpdate(dockerCli *client.DockerCli, flags *pflag.FlagSet, serviceID string) error {
client := dockerCli.Client()
apiClient := dockerCli.Client()
ctx := context.Background()
headers := map[string][]string{}
service, _, err := client.ServiceInspectWithRaw(ctx, serviceID)
service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID)
if err != nil {
return err
}
err = mergeService(&service.Spec, flags)
err = updateService(flags, &service.Spec)
if err != nil {
return err
}
err = client.ServiceUpdate(ctx, service.ID, service.Version, service.Spec)
// only send auth if flag was set
sendAuth, err := flags.GetBool(flagRegistryAuth)
if err != nil {
return err
}
if sendAuth {
// Retrieve encoded auth token from the image reference
// This would be the old image if it didn't change in this update
image := service.Spec.TaskTemplate.ContainerSpec.Image
encodedAuth, err := dockerCli.RetrieveAuthTokenFromImage(ctx, image)
if err != nil {
return err
}
headers["X-Registry-Auth"] = []string{encodedAuth}
}
err = apiClient.ServiceUpdate(ctx, service.ID, service.Version, service.Spec, headers)
if err != nil {
return err
}
@ -59,52 +76,52 @@ func runUpdate(dockerCli *client.DockerCli, flags *pflag.FlagSet, serviceID stri
return nil
}
func mergeService(spec *swarm.ServiceSpec, flags *pflag.FlagSet) error {
func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error {
mergeString := func(flag string, field *string) {
updateString := func(flag string, field *string) {
if flags.Changed(flag) {
*field, _ = flags.GetString(flag)
}
}
mergeListOpts := func(flag string, field *[]string) {
updateListOpts := func(flag string, field *[]string) {
if flags.Changed(flag) {
value := flags.Lookup(flag).Value.(*opts.ListOpts)
*field = value.GetAll()
}
}
mergeSlice := func(flag string, field *[]string) {
updateSlice := func(flag string, field *[]string) {
if flags.Changed(flag) {
*field, _ = flags.GetStringSlice(flag)
}
}
mergeInt64Value := func(flag string, field *int64) {
updateInt64Value := func(flag string, field *int64) {
if flags.Changed(flag) {
*field = flags.Lookup(flag).Value.(int64Value).Value()
}
}
mergeDuration := func(flag string, field *time.Duration) {
updateDuration := func(flag string, field *time.Duration) {
if flags.Changed(flag) {
*field, _ = flags.GetDuration(flag)
}
}
mergeDurationOpt := func(flag string, field *time.Duration) {
updateDurationOpt := func(flag string, field *time.Duration) {
if flags.Changed(flag) {
*field = *flags.Lookup(flag).Value.(*DurationOpt).Value()
}
}
mergeUint64 := func(flag string, field *uint64) {
updateUint64 := func(flag string, field *uint64) {
if flags.Changed(flag) {
*field, _ = flags.GetUint64(flag)
}
}
mergeUint64Opt := func(flag string, field *uint64) {
updateUint64Opt := func(flag string, field *uint64) {
if flags.Changed(flag) {
*field = *flags.Lookup(flag).Value.(*Uint64Opt).Value()
}
@ -112,37 +129,39 @@ func mergeService(spec *swarm.ServiceSpec, flags *pflag.FlagSet) error {
cspec := &spec.TaskTemplate.ContainerSpec
task := &spec.TaskTemplate
mergeString(flagName, &spec.Name)
mergeLabels(flags, &spec.Labels)
mergeString("image", &cspec.Image)
mergeSlice("command", &cspec.Command)
mergeSlice("arg", &cspec.Command)
mergeListOpts("env", &cspec.Env)
mergeString("workdir", &cspec.Dir)
mergeString("user", &cspec.User)
mergeMounts(flags, &cspec.Mounts)
if flags.Changed(flagLimitCPU) || flags.Changed(flagLimitMemory) {
taskResources := func() *swarm.ResourceRequirements {
if task.Resources == nil {
task.Resources = &swarm.ResourceRequirements{}
}
task.Resources.Limits = &swarm.Resources{}
mergeInt64Value(flagLimitCPU, &task.Resources.Limits.NanoCPUs)
mergeInt64Value(flagLimitMemory, &task.Resources.Limits.MemoryBytes)
return task.Resources
}
updateString(flagName, &spec.Name)
updateLabels(flags, &spec.Labels)
updateString("image", &cspec.Image)
updateSlice("command", &cspec.Command)
updateSlice("arg", &cspec.Args)
updateListOpts("env", &cspec.Env)
updateString("workdir", &cspec.Dir)
updateString(flagUser, &cspec.User)
updateMounts(flags, &cspec.Mounts)
if flags.Changed(flagLimitCPU) || flags.Changed(flagLimitMemory) {
taskResources().Limits = &swarm.Resources{}
updateInt64Value(flagLimitCPU, &task.Resources.Limits.NanoCPUs)
updateInt64Value(flagLimitMemory, &task.Resources.Limits.MemoryBytes)
}
if flags.Changed(flagReserveCPU) || flags.Changed(flagReserveMemory) {
if task.Resources == nil {
task.Resources = &swarm.ResourceRequirements{}
}
task.Resources.Reservations = &swarm.Resources{}
mergeInt64Value(flagReserveCPU, &task.Resources.Reservations.NanoCPUs)
mergeInt64Value(flagReserveMemory, &task.Resources.Reservations.MemoryBytes)
taskResources().Reservations = &swarm.Resources{}
updateInt64Value(flagReserveCPU, &task.Resources.Reservations.NanoCPUs)
updateInt64Value(flagReserveMemory, &task.Resources.Reservations.MemoryBytes)
}
mergeDurationOpt("stop-grace-period", cspec.StopGracePeriod)
updateDurationOpt(flagStopGracePeriod, cspec.StopGracePeriod)
if flags.Changed(flagRestartCondition) || flags.Changed(flagRestartDelay) || flags.Changed(flagRestartMaxAttempts) || flags.Changed(flagRestartWindow) {
if anyChanged(flags, flagRestartCondition, flagRestartDelay, flagRestartMaxAttempts, flagRestartWindow) {
if task.RestartPolicy == nil {
task.RestartPolicy = &swarm.RestartPolicy{}
}
@ -151,29 +170,29 @@ func mergeService(spec *swarm.ServiceSpec, flags *pflag.FlagSet) error {
value, _ := flags.GetString(flagRestartCondition)
task.RestartPolicy.Condition = swarm.RestartPolicyCondition(value)
}
mergeDurationOpt(flagRestartDelay, task.RestartPolicy.Delay)
mergeUint64Opt(flagRestartMaxAttempts, task.RestartPolicy.MaxAttempts)
mergeDurationOpt((flagRestartWindow), task.RestartPolicy.Window)
updateDurationOpt(flagRestartDelay, task.RestartPolicy.Delay)
updateUint64Opt(flagRestartMaxAttempts, task.RestartPolicy.MaxAttempts)
updateDurationOpt((flagRestartWindow), task.RestartPolicy.Window)
}
if flags.Changed(flagConstraint) {
task.Placement = &swarm.Placement{}
mergeSlice(flagConstraint, &task.Placement.Constraints)
updateSlice(flagConstraint, &task.Placement.Constraints)
}
if err := mergeMode(flags, &spec.Mode); err != nil {
if err := updateReplicas(flags, &spec.Mode); err != nil {
return err
}
if flags.Changed(flagUpdateParallelism) || flags.Changed(flagUpdateDelay) {
if anyChanged(flags, flagUpdateParallelism, flagUpdateDelay) {
if spec.UpdateConfig == nil {
spec.UpdateConfig = &swarm.UpdateConfig{}
}
mergeUint64(flagUpdateParallelism, &spec.UpdateConfig.Parallelism)
mergeDuration(flagUpdateDelay, &spec.UpdateConfig.Delay)
updateUint64(flagUpdateParallelism, &spec.UpdateConfig.Parallelism)
updateDuration(flagUpdateDelay, &spec.UpdateConfig.Delay)
}
mergeNetworks(flags, &spec.Networks)
updateNetworks(flags, &spec.Networks)
if flags.Changed(flagEndpointMode) {
value, _ := flags.GetString(flagEndpointMode)
spec.EndpointSpec.Mode = swarm.ResolutionMode(value)
@ -183,38 +202,45 @@ func mergeService(spec *swarm.ServiceSpec, flags *pflag.FlagSet) error {
if spec.EndpointSpec == nil {
spec.EndpointSpec = &swarm.EndpointSpec{}
}
mergePorts(flags, &spec.EndpointSpec.Ports)
updatePorts(flags, &spec.EndpointSpec.Ports)
}
return nil
}
func mergeLabels(flags *pflag.FlagSet, field *map[string]string) {
func updateLabels(flags *pflag.FlagSet, field *map[string]string) {
if !flags.Changed(flagLabel) {
return
}
if *field == nil {
*field = make(map[string]string)
}
values := flags.Lookup(flagLabel).Value.(*opts.ListOpts).GetAll()
localLabels := map[string]string{}
for key, value := range runconfigopts.ConvertKVStringsToMap(values) {
(*field)[key] = value
localLabels[key] = value
}
*field = localLabels
}
func anyChanged(flags *pflag.FlagSet, fields ...string) bool {
for _, flag := range fields {
if flags.Changed(flag) {
return true
}
}
return false
}
// TODO: should this override by destination path, or does swarm handle that?
func mergeMounts(flags *pflag.FlagSet, mounts *[]swarm.Mount) {
func updateMounts(flags *pflag.FlagSet, mounts *[]swarm.Mount) {
if !flags.Changed(flagMount) {
return
}
values := flags.Lookup(flagMount).Value.(*MountOpt).Value()
*mounts = append(*mounts, values...)
*mounts = flags.Lookup(flagMount).Value.(*MountOpt).Value()
}
// TODO: should this override by name, or does swarm handle that?
func mergePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) {
func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) {
if !flags.Changed(flagPublish) {
return
}
@ -222,55 +248,34 @@ func mergePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) {
values := flags.Lookup(flagPublish).Value.(*opts.ListOpts).GetAll()
ports, portBindings, _ := nat.ParsePortSpecs(values)
var localPortConfig []swarm.PortConfig
for port := range ports {
*portConfig = append(*portConfig, convertPortToPortConfig(port, portBindings)...)
localPortConfig = append(localPortConfig, convertPortToPortConfig(port, portBindings)...)
}
*portConfig = localPortConfig
}
func mergeNetworks(flags *pflag.FlagSet, attachments *[]swarm.NetworkAttachmentConfig) {
func updateNetworks(flags *pflag.FlagSet, attachments *[]swarm.NetworkAttachmentConfig) {
if !flags.Changed(flagNetwork) {
return
}
networks, _ := flags.GetStringSlice(flagNetwork)
var localAttachments []swarm.NetworkAttachmentConfig
for _, network := range networks {
*attachments = append(*attachments, swarm.NetworkAttachmentConfig{Target: network})
localAttachments = append(localAttachments, swarm.NetworkAttachmentConfig{Target: network})
}
*attachments = localAttachments
}
func mergeMode(flags *pflag.FlagSet, serviceMode *swarm.ServiceMode) error {
if !flags.Changed(flagMode) && !flags.Changed(flagReplicas) {
func updateReplicas(flags *pflag.FlagSet, serviceMode *swarm.ServiceMode) error {
if !flags.Changed(flagReplicas) {
return nil
}
var mode string
if flags.Changed(flagMode) {
mode, _ = flags.GetString(flagMode)
}
if !(mode == "replicated" || serviceMode.Replicated != nil) && flags.Changed(flagReplicas) {
if serviceMode.Replicated == nil {
return fmt.Errorf("replicas can only be used with replicated mode")
}
if mode == "global" {
serviceMode.Replicated = nil
serviceMode.Global = &swarm.GlobalService{}
return nil
}
if flags.Changed(flagReplicas) {
replicas := flags.Lookup(flagReplicas).Value.(*Uint64Opt).Value()
serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas}
serviceMode.Global = nil
return nil
}
if mode == "replicated" {
if serviceMode.Replicated != nil {
return nil
}
serviceMode.Replicated = &swarm.ReplicatedService{Replicas: &DefaultReplicas}
serviceMode.Global = nil
}
serviceMode.Replicated.Replicas = flags.Lookup(flagReplicas).Value.(*Uint64Opt).Value()
return nil
}

View file

@ -0,0 +1,26 @@
package service
import (
"testing"
"github.com/docker/docker/pkg/testutil/assert"
"github.com/docker/engine-api/types/swarm"
)
func TestUpdateServiceCommandAndArgs(t *testing.T) {
flags := newUpdateCommand(nil).Flags()
flags.Set("command", "the")
flags.Set("command", "new")
flags.Set("command", "command")
flags.Set("arg", "the")
flags.Set("arg", "new args")
spec := &swarm.ServiceSpec{}
cspec := &spec.TaskTemplate.ContainerSpec
cspec.Command = []string{"old", "command"}
cspec.Args = []string{"old", "args"}
updateService(flags, spec)
assert.EqualStringSlice(t, cspec.Command, []string{"the", "new", "command"})
assert.EqualStringSlice(t, cspec.Args, []string{"the", "new args"})
}

View file

@ -7,12 +7,12 @@ import (
"github.com/spf13/cobra"
)
// NewStackCommand returns nocommand
// NewStackCommand returns no command
func NewStackCommand(dockerCli *client.DockerCli) *cobra.Command {
return &cobra.Command{}
}
// NewTopLevelDeployCommand return no command
// NewTopLevelDeployCommand returns no command
func NewTopLevelDeployCommand(dockerCli *client.DockerCli) *cobra.Command {
return &cobra.Command{}
}

View file

@ -31,7 +31,7 @@ func newDeployCommand(dockerCli *client.DockerCli) *cobra.Command {
cmd := &cobra.Command{
Use: "deploy [OPTIONS] STACK",
Aliases: []string{"up"},
Short: "Create and update a stack",
Short: "Create and update a stack from a Distributed Application Bundle (DAB)",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.namespace = args[0]
@ -184,18 +184,21 @@ func deployServices(
if service, exists := existingServiceMap[name]; exists {
fmt.Fprintf(out, "Updating service %s (id: %s)\n", name, service.ID)
// TODO(nishanttotla): Pass headers with X-Registry-Auth
if err := apiClient.ServiceUpdate(
ctx,
service.ID,
service.Version,
serviceSpec,
nil,
); err != nil {
return err
}
} else {
fmt.Fprintf(out, "Creating service %s\n", name)
if _, err := apiClient.ServiceCreate(ctx, serviceSpec); err != nil {
// TODO(nishanttotla): Pass headers with X-Registry-Auth
if _, err := apiClient.ServiceCreate(ctx, serviceSpec, nil); err != nil {
return err
}
}

View file

@ -15,11 +15,11 @@ func addBundlefileFlag(opt *string, flags *pflag.FlagSet) {
flags.StringVarP(
opt,
"bundle", "f", "",
"Path to a bundle (Default: STACK.dsb)")
"Path to a Distributed Application Bundle file (Default: STACK.dab)")
}
func loadBundlefile(stderr io.Writer, namespace string, path string) (*bundlefile.Bundlefile, error) {
defaultPath := fmt.Sprintf("%s.dsb", namespace)
defaultPath := fmt.Sprintf("%s.dab", namespace)
if path == "" {
path = defaultPath

View file

@ -13,17 +13,17 @@ import (
)
type initOptions struct {
swarmOptions
listenAddr NodeAddrOption
autoAccept AutoAcceptOption
forceNewCluster bool
secret string
}
func newInitCommand(dockerCli *client.DockerCli) *cobra.Command {
var flags *pflag.FlagSet
opts := initOptions{
listenAddr: NewNodeAddrOption(),
autoAccept: NewAutoAcceptOption(),
listenAddr: NewListenAddrOption(),
swarmOptions: swarmOptions{
autoAccept: NewAutoAcceptOption(),
},
}
cmd := &cobra.Command{
@ -31,15 +31,14 @@ func newInitCommand(dockerCli *client.DockerCli) *cobra.Command {
Short: "Initialize a Swarm",
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runInit(dockerCli, flags, opts)
return runInit(dockerCli, cmd.Flags(), opts)
},
}
flags = cmd.Flags()
flags := cmd.Flags()
flags.Var(&opts.listenAddr, "listen-addr", "Listen address")
flags.Var(&opts.autoAccept, "auto-accept", "Auto acceptance policy (worker, manager, or none)")
flags.StringVar(&opts.secret, "secret", "", "Set secret value needed to accept nodes into cluster")
flags.BoolVar(&opts.forceNewCluster, "force-new-cluster", false, "Force create a new cluster from current state.")
addSwarmFlags(flags, &opts.swarmOptions)
return cmd
}
@ -50,13 +49,9 @@ func runInit(dockerCli *client.DockerCli, flags *pflag.FlagSet, opts initOptions
req := swarm.InitRequest{
ListenAddr: opts.listenAddr.String(),
ForceNewCluster: opts.forceNewCluster,
Spec: opts.swarmOptions.ToSpec(),
}
if flags.Changed("secret") {
req.Spec.AcceptancePolicy.Policies = opts.autoAccept.Policies(&opts.secret)
} else {
req.Spec.AcceptancePolicy.Policies = opts.autoAccept.Policies(nil)
}
nodeID, err := client.SwarmInit(ctx, req)
if err != nil {
return err

View file

@ -20,7 +20,7 @@ type joinOptions struct {
func newJoinCommand(dockerCli *client.DockerCli) *cobra.Command {
opts := joinOptions{
listenAddr: NodeAddrOption{addr: defaultListenAddr},
listenAddr: NewListenAddrOption(),
}
cmd := &cobra.Command{
@ -34,9 +34,9 @@ func newJoinCommand(dockerCli *client.DockerCli) *cobra.Command {
}
flags := cmd.Flags()
flags.Var(&opts.listenAddr, "listen-addr", "Listen address")
flags.Var(&opts.listenAddr, flagListenAddr, "Listen address")
flags.BoolVar(&opts.manager, "manager", false, "Try joining as a manager.")
flags.StringVar(&opts.secret, "secret", "", "Secret for node acceptance")
flags.StringVar(&opts.secret, flagSecret, "", "Secret for node acceptance")
flags.StringVar(&opts.CACertHash, "ca-hash", "", "Hash of the Root Certificate Authority certificate used for trusted join")
return cmd
}

View file

@ -39,6 +39,6 @@ func runLeave(dockerCli *client.DockerCli, opts leaveOptions) error {
return err
}
fmt.Fprintln(dockerCli.Out(), "Node left the default swarm.")
fmt.Fprintln(dockerCli.Out(), "Node left the swarm.")
return nil
}

View file

@ -1,10 +1,15 @@
package swarm
import (
"encoding/csv"
"errors"
"fmt"
"strings"
"time"
"github.com/docker/docker/opts"
"github.com/docker/engine-api/types/swarm"
"github.com/spf13/pflag"
)
const (
@ -13,6 +18,14 @@ const (
WORKER = "WORKER"
// MANAGER constant for manager name
MANAGER = "MANAGER"
flagAutoAccept = "auto-accept"
flagCertExpiry = "cert-expiry"
flagDispatcherHeartbeat = "dispatcher-heartbeat"
flagListenAddr = "listen-addr"
flagSecret = "secret"
flagTaskHistoryLimit = "task-history-limit"
flagExternalCA = "external-ca"
)
var (
@ -22,6 +35,15 @@ var (
}
)
type swarmOptions struct {
autoAccept AutoAcceptOption
secret string
taskHistoryLimit int64
dispatcherHeartbeat time.Duration
nodeCertExpiry time.Duration
externalCA ExternalCAOption
}
// NodeAddrOption is a pflag.Value for listen and remote addresses
type NodeAddrOption struct {
addr string
@ -29,21 +51,16 @@ type NodeAddrOption struct {
// String prints the representation of this flag
func (a *NodeAddrOption) String() string {
return a.addr
return a.Value()
}
// Set the value for this flag
func (a *NodeAddrOption) Set(value string) error {
if !strings.Contains(value, ":") {
return fmt.Errorf("Invalid url, a host and port are required")
addr, err := opts.ParseTCPAddr(value, a.addr)
if err != nil {
return err
}
parts := strings.Split(value, ":")
if len(parts) != 2 {
return fmt.Errorf("Invalid url, too many colons")
}
a.addr = value
a.addr = addr
return nil
}
@ -52,9 +69,19 @@ func (a *NodeAddrOption) Type() string {
return "node-addr"
}
// Value returns the value of this option as addr:port
func (a *NodeAddrOption) Value() string {
return strings.TrimPrefix(a.addr, "tcp://")
}
// NewNodeAddrOption returns a new node address option
func NewNodeAddrOption() NodeAddrOption {
return NodeAddrOption{addr: defaultListenAddr}
func NewNodeAddrOption(addr string) NodeAddrOption {
return NodeAddrOption{addr}
}
// NewListenAddrOption returns a NodeAddrOption with default values
func NewListenAddrOption() NodeAddrOption {
return NewNodeAddrOption(defaultListenAddr)
}
// AutoAcceptOption is a value type for auto-accept policy
@ -65,10 +92,10 @@ type AutoAcceptOption struct {
// String prints a string representation of this option
func (o *AutoAcceptOption) String() string {
keys := []string{}
for key := range o.values {
keys = append(keys, key)
for key, value := range o.values {
keys = append(keys, fmt.Sprintf("%s=%v", strings.ToLower(key), value))
}
return strings.Join(keys, " ")
return strings.Join(keys, ", ")
}
// Set sets a new value on this option
@ -118,3 +145,115 @@ func (o *AutoAcceptOption) Policies(secret *string) []swarm.Policy {
func NewAutoAcceptOption() AutoAcceptOption {
return AutoAcceptOption{values: make(map[string]bool)}
}
// ExternalCAOption is a Value type for parsing external CA specifications.
type ExternalCAOption struct {
values []*swarm.ExternalCA
}
// Set parses an external CA option.
func (m *ExternalCAOption) Set(value string) error {
parsed, err := parseExternalCA(value)
if err != nil {
return err
}
m.values = append(m.values, parsed)
return nil
}
// Type returns the type of this option.
func (m *ExternalCAOption) Type() string {
return "external-ca"
}
// String returns a string repr of this option.
func (m *ExternalCAOption) String() string {
externalCAs := []string{}
for _, externalCA := range m.values {
repr := fmt.Sprintf("%s: %s", externalCA.Protocol, externalCA.URL)
externalCAs = append(externalCAs, repr)
}
return strings.Join(externalCAs, ", ")
}
// Value returns the external CAs
func (m *ExternalCAOption) Value() []*swarm.ExternalCA {
return m.values
}
// parseExternalCA parses an external CA specification from the command line,
// such as protocol=cfssl,url=https://example.com.
func parseExternalCA(caSpec string) (*swarm.ExternalCA, error) {
csvReader := csv.NewReader(strings.NewReader(caSpec))
fields, err := csvReader.Read()
if err != nil {
return nil, err
}
externalCA := swarm.ExternalCA{
Options: make(map[string]string),
}
var (
hasProtocol bool
hasURL bool
)
for _, field := range fields {
parts := strings.SplitN(field, "=", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid field '%s' must be a key=value pair", field)
}
key, value := parts[0], parts[1]
switch strings.ToLower(key) {
case "protocol":
hasProtocol = true
if strings.ToLower(value) == string(swarm.ExternalCAProtocolCFSSL) {
externalCA.Protocol = swarm.ExternalCAProtocolCFSSL
} else {
return nil, fmt.Errorf("unrecognized external CA protocol %s", value)
}
case "url":
hasURL = true
externalCA.URL = value
default:
externalCA.Options[key] = value
}
}
if !hasProtocol {
return nil, errors.New("the external-ca option needs a protocol= parameter")
}
if !hasURL {
return nil, errors.New("the external-ca option needs a url= parameter")
}
return &externalCA, nil
}
func addSwarmFlags(flags *pflag.FlagSet, opts *swarmOptions) {
flags.Var(&opts.autoAccept, flagAutoAccept, "Auto acceptance policy (worker, manager or none)")
flags.StringVar(&opts.secret, flagSecret, "", "Set secret value needed to accept nodes into cluster")
flags.Int64Var(&opts.taskHistoryLimit, flagTaskHistoryLimit, 10, "Task history retention limit")
flags.DurationVar(&opts.dispatcherHeartbeat, flagDispatcherHeartbeat, time.Duration(5*time.Second), "Dispatcher heartbeat period")
flags.DurationVar(&opts.nodeCertExpiry, flagCertExpiry, time.Duration(90*24*time.Hour), "Validity period for node certificates")
flags.Var(&opts.externalCA, flagExternalCA, "Specifications of one or more certificate signing endpoints")
}
func (opts *swarmOptions) ToSpec() swarm.Spec {
spec := swarm.Spec{}
if opts.secret != "" {
spec.AcceptancePolicy.Policies = opts.autoAccept.Policies(&opts.secret)
} else {
spec.AcceptancePolicy.Policies = opts.autoAccept.Policies(nil)
}
spec.Orchestration.TaskHistoryRetentionLimit = opts.taskHistoryLimit
spec.Dispatcher.HeartbeatPeriod = uint64(opts.dispatcherHeartbeat.Nanoseconds())
spec.CAConfig.NodeCertExpiry = opts.nodeCertExpiry
spec.CAConfig.ExternalCAs = opts.externalCA.Value()
return spec
}

View file

@ -0,0 +1,120 @@
package swarm
import (
"testing"
"github.com/docker/docker/pkg/testutil/assert"
"github.com/docker/engine-api/types/swarm"
)
func TestNodeAddrOptionSetHostAndPort(t *testing.T) {
opt := NewNodeAddrOption("old:123")
addr := "newhost:5555"
assert.NilError(t, opt.Set(addr))
assert.Equal(t, opt.Value(), addr)
}
func TestNodeAddrOptionSetHostOnly(t *testing.T) {
opt := NewListenAddrOption()
assert.NilError(t, opt.Set("newhost"))
assert.Equal(t, opt.Value(), "newhost:2377")
}
func TestNodeAddrOptionSetHostOnlyIPv6(t *testing.T) {
opt := NewListenAddrOption()
assert.NilError(t, opt.Set("::1"))
assert.Equal(t, opt.Value(), "[::1]:2377")
}
func TestNodeAddrOptionSetPortOnly(t *testing.T) {
opt := NewListenAddrOption()
assert.NilError(t, opt.Set(":4545"))
assert.Equal(t, opt.Value(), "0.0.0.0:4545")
}
func TestNodeAddrOptionSetInvalidFormat(t *testing.T) {
opt := NewListenAddrOption()
assert.Error(t, opt.Set("http://localhost:4545"), "Invalid")
}
func TestAutoAcceptOptionSetWorker(t *testing.T) {
opt := NewAutoAcceptOption()
assert.NilError(t, opt.Set("worker"))
assert.Equal(t, opt.values[WORKER], true)
}
func TestAutoAcceptOptionSetManager(t *testing.T) {
opt := NewAutoAcceptOption()
assert.NilError(t, opt.Set("manager"))
assert.Equal(t, opt.values[MANAGER], true)
}
func TestAutoAcceptOptionSetInvalid(t *testing.T) {
opt := NewAutoAcceptOption()
assert.Error(t, opt.Set("bogus"), "must be one of")
}
func TestAutoAcceptOptionSetNone(t *testing.T) {
opt := NewAutoAcceptOption()
assert.NilError(t, opt.Set("none"))
assert.Equal(t, opt.values[MANAGER], false)
assert.Equal(t, opt.values[WORKER], false)
}
func TestAutoAcceptOptionSetConflict(t *testing.T) {
opt := NewAutoAcceptOption()
assert.NilError(t, opt.Set("manager"))
assert.Error(t, opt.Set("none"), "value NONE is incompatible with MANAGER")
opt = NewAutoAcceptOption()
assert.NilError(t, opt.Set("none"))
assert.Error(t, opt.Set("worker"), "value NONE is incompatible with WORKER")
}
func TestAutoAcceptOptionPoliciesDefault(t *testing.T) {
opt := NewAutoAcceptOption()
secret := "thesecret"
policies := opt.Policies(&secret)
assert.Equal(t, len(policies), 2)
assert.Equal(t, policies[0], swarm.Policy{
Role: WORKER,
Autoaccept: true,
Secret: &secret,
})
assert.Equal(t, policies[1], swarm.Policy{
Role: MANAGER,
Autoaccept: false,
Secret: &secret,
})
}
func TestAutoAcceptOptionPoliciesWithManager(t *testing.T) {
opt := NewAutoAcceptOption()
secret := "thesecret"
assert.NilError(t, opt.Set("manager"))
policies := opt.Policies(&secret)
assert.Equal(t, len(policies), 2)
assert.Equal(t, policies[0], swarm.Policy{
Role: WORKER,
Autoaccept: false,
Secret: &secret,
})
assert.Equal(t, policies[1], swarm.Policy{
Role: MANAGER,
Autoaccept: true,
Secret: &secret,
})
}
func TestAutoAcceptOptionString(t *testing.T) {
opt := NewAutoAcceptOption()
assert.NilError(t, opt.Set("manager"))
assert.NilError(t, opt.Set("worker"))
repr := opt.String()
assert.Contains(t, repr, "worker=true")
assert.Contains(t, repr, "manager=true")
}

View file

@ -2,7 +2,6 @@ package swarm
import (
"fmt"
"time"
"golang.org/x/net/context"
@ -13,37 +12,23 @@ import (
"github.com/spf13/pflag"
)
type updateOptions struct {
autoAccept AutoAcceptOption
secret string
taskHistoryLimit int64
dispatcherHeartbeat time.Duration
nodeCertExpiry time.Duration
}
func newUpdateCommand(dockerCli *client.DockerCli) *cobra.Command {
opts := updateOptions{autoAccept: NewAutoAcceptOption()}
var flags *pflag.FlagSet
opts := swarmOptions{autoAccept: NewAutoAcceptOption()}
cmd := &cobra.Command{
Use: "update",
Short: "Update the Swarm",
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runUpdate(dockerCli, flags, opts)
return runUpdate(dockerCli, cmd.Flags(), opts)
},
}
flags = cmd.Flags()
flags.Var(&opts.autoAccept, "auto-accept", "Auto acceptance policy (worker, manager or none)")
flags.StringVar(&opts.secret, "secret", "", "Set secret value needed to accept nodes into cluster")
flags.Int64Var(&opts.taskHistoryLimit, "task-history-limit", 10, "Task history retention limit")
flags.DurationVar(&opts.dispatcherHeartbeat, "dispatcher-heartbeat", time.Duration(5*time.Second), "Dispatcher heartbeat period")
flags.DurationVar(&opts.nodeCertExpiry, "cert-expiry", time.Duration(90*24*time.Hour), "Validity period for node certificates")
addSwarmFlags(cmd.Flags(), &opts)
return cmd
}
func runUpdate(dockerCli *client.DockerCli, flags *pflag.FlagSet, opts updateOptions) error {
func runUpdate(dockerCli *client.DockerCli, flags *pflag.FlagSet, opts swarmOptions) error {
client := dockerCli.Client()
ctx := context.Background()
@ -69,14 +54,14 @@ func runUpdate(dockerCli *client.DockerCli, flags *pflag.FlagSet, opts updateOpt
func mergeSwarm(swarm *swarm.Swarm, flags *pflag.FlagSet) error {
spec := &swarm.Spec
if flags.Changed("auto-accept") {
value := flags.Lookup("auto-accept").Value.(*AutoAcceptOption)
if flags.Changed(flagAutoAccept) {
value := flags.Lookup(flagAutoAccept).Value.(*AutoAcceptOption)
spec.AcceptancePolicy.Policies = value.Policies(nil)
}
var psecret *string
if flags.Changed("secret") {
secret, _ := flags.GetString("secret")
if flags.Changed(flagSecret) {
secret, _ := flags.GetString(flagSecret)
psecret = &secret
}
@ -84,21 +69,26 @@ func mergeSwarm(swarm *swarm.Swarm, flags *pflag.FlagSet) error {
spec.AcceptancePolicy.Policies[i].Secret = psecret
}
if flags.Changed("task-history-limit") {
spec.Orchestration.TaskHistoryRetentionLimit, _ = flags.GetInt64("task-history-limit")
if flags.Changed(flagTaskHistoryLimit) {
spec.Orchestration.TaskHistoryRetentionLimit, _ = flags.GetInt64(flagTaskHistoryLimit)
}
if flags.Changed("dispatcher-heartbeat") {
if v, err := flags.GetDuration("dispatcher-heartbeat"); err == nil {
if flags.Changed(flagDispatcherHeartbeat) {
if v, err := flags.GetDuration(flagDispatcherHeartbeat); err == nil {
spec.Dispatcher.HeartbeatPeriod = uint64(v.Nanoseconds())
}
}
if flags.Changed("cert-expiry") {
if v, err := flags.GetDuration("cert-expiry"); err == nil {
if flags.Changed(flagCertExpiry) {
if v, err := flags.GetDuration(flagCertExpiry); err == nil {
spec.CAConfig.NodeCertExpiry = v
}
}
if flags.Changed(flagExternalCA) {
value := flags.Lookup(flagExternalCA).Value.(*ExternalCAOption)
spec.CAConfig.ExternalCAs = value.Value()
}
return nil
}

View file

@ -134,7 +134,7 @@ func CopyToFile(outfile string, r io.Reader) error {
return nil
}
// ForwardAllSignals forwards signals to the contianer
// ForwardAllSignals forwards signals to the container
// TODO: this can be unexported again once all container commands are under
// api/client/container
func (cli *DockerCli) ForwardAllSignals(ctx context.Context, cid string) chan os.Signal {

View file

@ -19,7 +19,7 @@ func newInspectCommand(dockerCli *client.DockerCli) *cobra.Command {
cmd := &cobra.Command{
Use: "inspect [OPTIONS] VOLUME [VOLUME...]",
Short: "Return low-level information on a volume",
Short: "Display detailed information on one or more volumes",
Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.names = args

View file

@ -53,7 +53,7 @@ func filterNetworks(nws []types.NetworkResource, filter filters.Args) ([]types.N
return nil, err
}
var displayNet []types.NetworkResource
displayNet := []types.NetworkResource{}
for _, nw := range nws {
if filter.Include("driver") {
if !filter.ExactMatch("driver", nw.Driver) {

View file

@ -14,8 +14,8 @@ type Backend interface {
Update(uint64, types.Spec) error
GetServices(basictypes.ServiceListOptions) ([]types.Service, error)
GetService(string) (types.Service, error)
CreateService(types.ServiceSpec) (string, error)
UpdateService(string, uint64, types.ServiceSpec) error
CreateService(types.ServiceSpec, string) (string, error)
UpdateService(string, uint64, types.ServiceSpec, string) error
RemoveService(string) error
GetNodes(basictypes.NodeListOptions) ([]types.Node, error)
GetNode(string) (types.Node, error)

View file

@ -107,9 +107,12 @@ func (sr *swarmRouter) createService(ctx context.Context, w http.ResponseWriter,
return err
}
id, err := sr.backend.CreateService(service)
// Get returns "" if the header does not exist
encodedAuth := r.Header.Get("X-Registry-Auth")
id, err := sr.backend.CreateService(service, encodedAuth)
if err != nil {
logrus.Errorf("Error reating service %s: %v", id, err)
logrus.Errorf("Error creating service %s: %v", id, err)
return err
}
@ -130,7 +133,10 @@ func (sr *swarmRouter) updateService(ctx context.Context, w http.ResponseWriter,
return fmt.Errorf("Invalid service version '%s': %s", rawVersion, err.Error())
}
if err := sr.backend.UpdateService(vars["id"], version, service); err != nil {
// Get returns "" if the header does not exist
encodedAuth := r.Header.Get("X-Registry-Auth")
if err := sr.backend.UpdateService(vars["id"], version, service, encodedAuth); err != nil {
logrus.Errorf("Error updating service %s: %v", vars["id"], err)
return err
}

View file

@ -172,7 +172,7 @@ func (b *Builder) runContextCommand(args []string, allowRemote bool, allowLocalD
}
cmd := b.runConfig.Cmd
b.runConfig.Cmd = strslice.StrSlice(append(getShell(b.runConfig), "#(nop) %s %s in %s ", cmdName, srcHash, dest))
b.runConfig.Cmd = strslice.StrSlice(append(getShell(b.runConfig), fmt.Sprintf("#(nop) %s %s in %s ", cmdName, srcHash, dest)))
defer func(cmd strslice.StrSlice) { b.runConfig.Cmd = cmd }(cmd)
if hit, err := b.probeCache(); err != nil {

View file

@ -0,0 +1,18 @@
#
# THIS FILE IS AUTOGENERATED; SEE "contrib/builder/rpm/amd64/generate.sh"!
#
FROM fedora:24
RUN dnf install -y @development-tools fedora-packager
RUN dnf install -y btrfs-progs-devel device-mapper-devel glibc-static libseccomp-devel libselinux-devel libtool-ltdl-devel pkgconfig selinux-policy selinux-policy-devel sqlite-devel systemd-devel tar git
ENV GO_VERSION 1.6.2
RUN curl -fSL "https://storage.googleapis.com/golang/go${GO_VERSION}.linux-amd64.tar.gz" | tar xzC /usr/local
ENV PATH $PATH:/usr/local/go/bin
ENV AUTO_GOPATH 1
ENV DOCKER_BUILDTAGS pkcs11 seccomp selinux
ENV RUNC_BUILDTAGS seccomp selinux

View file

@ -232,6 +232,7 @@ flags=(
CGROUP_HUGETLB
NET_CLS_CGROUP $netprio
CFS_BANDWIDTH FAIR_GROUP_SCHED RT_GROUP_SCHED
IP_VS
)
check_flags "${flags[@]}"
@ -249,6 +250,8 @@ echo '- Network Drivers:'
{
echo '- "'$(wrap_color 'overlay' blue)'":'
check_flags VXLAN | sed 's/^/ /'
echo ' Optional (for secure networks):'
check_flags XFRM_ALGO XFRM_USER | sed 's/^/ /'
} | sed 's/^/ /'
echo '- Storage Drivers:'

View file

@ -693,7 +693,7 @@ _docker_build() {
--cgroup-parent
--cpuset-cpus
--cpuset-mems
--cpu-shares
--cpu-shares -c
--cpu-period
--cpu-quota
--file -f
@ -942,10 +942,11 @@ _docker_daemon() {
return
;;
--storage-driver|-s)
COMPREPLY=( $( compgen -W "aufs btrfs devicemapper overlay vfs zfs" -- "$(echo $cur | tr '[:upper:]' '[:lower:]')" ) )
COMPREPLY=( $( compgen -W "aufs btrfs devicemapper overlay overlay2 vfs zfs" -- "$(echo $cur | tr '[:upper:]' '[:lower:]')" ) )
return
;;
--storage-opt)
local btrfs_options="btrfs.min_space"
local devicemapper_options="
dm.basesize
dm.blkdiscard
@ -965,7 +966,10 @@ _docker_daemon() {
case $(__docker_value_of_option '--storage-driver|-s') in
'')
COMPREPLY=( $( compgen -W "$devicemapper_options $zfs_options" -S = -- "$cur" ) )
COMPREPLY=( $( compgen -W "$btrfs_options $devicemapper_options $zfs_options" -S = -- "$cur" ) )
;;
btrfs)
COMPREPLY=( $( compgen -W "$btrfs_options" -S = -- "$cur" ) )
;;
devicemapper)
COMPREPLY=( $( compgen -W "$devicemapper_options" -S = -- "$cur" ) )
@ -1049,6 +1053,7 @@ _docker_events() {
export
import
kill
load
mount
oom
pause
@ -1058,6 +1063,7 @@ _docker_events() {
rename
resize
restart
save
start
stop
tag
@ -1355,6 +1361,7 @@ _docker_network_connect() {
--ip
--ip6
--link
--link-local-ip
"
local boolean_options="
@ -1528,11 +1535,12 @@ _docker_network() {
_docker_service() {
local subcommands="
create
tasks
inspect
ls list
rm remove
scale
tasks
update
ls
rm
"
__docker_subcommands "$subcommands" && return
@ -1547,43 +1555,47 @@ _docker_service() {
}
_docker_service_create() {
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--help --constraint --endpoint-ingress --endpoint-mode --env --label --limit-cpu --limit-memory --mode --name --network --publish --reserve-cpu --reserve-memory --restart-condition --restart-delay --restart-max-attempts --restart-window --replicas --stop-grace-period --update-delay --update-parallelism --user --volume --workdir" -- "$cur" ) )
;;
esac
}
_docker_service_update() {
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--arg --command --constraint --endpoint-ingress --endpoint-mode --env --help --image --label --limit-cpu --limit-memory --mode --name --network --publish --reserve-cpu --reserve-memory --restart-condition--restart-delay --restart-max-attempts --restart-window --replicas --stop-grace-period --update-delay --update-parallelism --user --volume --workdir" -- "$cur" ) )
;;
*)
__docker_complete_services
esac
_docker_service_update
}
_docker_service_inspect() {
case "$prev" in
--format|-f)
return
;;
esac
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--help --format --pretty" -- "$cur" ) )
COMPREPLY=( $( compgen -W "--format -f --help --pretty -p" -- "$cur" ) )
;;
*)
__docker_complete_services
esac
}
_docker_service_tasks() {
_docker_service_list() {
_docker_service_ls
}
_docker_service_ls() {
case "$prev" in
--format|-f)
return
;;
esac
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--help --all --filter --no-resolve" -- "$cur" ) )
COMPREPLY=( $( compgen -W "-f --filter --help --quiet -q" -- "$cur" ) )
;;
*)
__docker_complete_services
esac
}
_docker_service_remove() {
_docker_service_rm
}
_docker_service_rm() {
case "$cur" in
-*)
@ -1594,21 +1606,131 @@ _docker_service_rm() {
esac
}
_docker_service_ls() {
_docker_service_scale() {
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--help" -- "$cur" ) )
;;
*)
COMPREPLY=( $(compgen -S "=" -W "$(__docker_services $1)" -- "$cur") )
__docker_nospace
;;
esac
}
_docker_service_tasks() {
case "$prev" in
--format|-f)
return
;;
esac
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--all -a --filter -f --help --no-resolve -n" -- "$cur" ) )
;;
*)
__docker_complete_services
esac
}
_docker_service_update() {
local $subcommand="${words[$subcommand_pos]}"
local options_with_args="
--constraint
--endpoint-mode
--env -e
--label -l
--limit-cpu
--limit-memory
--mode
--mount -m
--name
--network
--publish -p
--replicas
--reserve-cpu
--reserve-memory
--restart-condition
--restart-delay
--restart-max-attempts
--restart-window
--stop-grace-period
--update-delay
--update-parallelism
--user -u
--workdir -w
"
local boolean_options="
--help
"
if [ "$subcommand" = "update" ] ; then
options_with_args="$options_with_args
--arg
--command
--image
"
case "$prev" in
--image)
__docker_complete_image_repos_and_tags
return
;;
esac
fi
case "$prev" in
--endpoint-mode)
COMPREPLY=( $( compgen -W "DNSRR VIP" -- "$cur" ) )
return
;;
--env|-e)
COMPREPLY=( $( compgen -e -S = -- "$cur" ) )
__docker_nospace
return
;;
--mode)
COMPREPLY=( $( compgen -W "global replicated" -- "$cur" ) )
return
;;
--network)
__docker_complete_networks
return
;;
--restart-condition)
COMPREPLY=( $( compgen -W "any none on_failure" -- "$cur" ) )
return
;;
--user|-u)
__docker_complete_user_group
return
;;
$(__docker_to_extglob "$options_with_args") )
return
;;
esac
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "$boolean_options $options_with_args" -- "$cur" ) )
;;
*)
if [ "$subcommand" = "update" ] ; then
__docker_complete_services
fi
esac
}
_docker_swarm() {
local subcommands="
init
join
update
leave
inspect
join
leave
update
"
__docker_subcommands "$subcommands" && return
@ -1623,31 +1745,47 @@ _docker_swarm() {
}
_docker_swarm_init() {
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--help --auto-accept --force-new-cluster --secret" -- "$cur" ) )
;;
esac
}
_docker_swarm_join() {
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--ca-hash --help --listen-addr --manager --secret" -- "$cur" ) )
;;
esac
}
_docker_swarm_update() {
case "$prev" in
--auto-accept|--cert-expiry|--dispatcher-heartbeat|--secret|--task-history-limit)
--auto-accept)
COMPREPLY=( $( compgen -W "manager none worker" -- "$cur" ) )
return
;;
--listen-addr|--secret)
return
;;
esac
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--auto-accept --cert-expiry --dispatcher-heartbeat --help --secret --task-history-limit" -- "$cur" ) )
COMPREPLY=( $( compgen -W "--auto-accept --force-new-cluster --help --listen-addr --secret" -- "$cur" ) )
;;
esac
}
_docker_swarm_inspect() {
case "$prev" in
--format|-f)
return
;;
esac
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--format -f --help" -- "$cur" ) )
;;
esac
}
_docker_swarm_join() {
case "$prev" in
--ca-hash|--listen-addr|--secret)
return
;;
esac
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--ca-hash --help --listen-addr --manager --secret" -- "$cur" ) )
;;
esac
}
@ -1660,10 +1798,20 @@ _docker_swarm_leave() {
esac
}
_docker_swarm_inspect() {
_docker_swarm_update() {
case "$prev" in
--auto-accept)
COMPREPLY=( $( compgen -W "manager none worker" -- "$cur" ) )
return
;;
--cert-expiry|--dispatcher-heartbeat|--secret|--task-history-limit)
return
;;
esac
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--format --help" -- "$cur" ) )
COMPREPLY=( $( compgen -W "--auto-accept --cert-expiry --dispatcher-heartbeat --help --secret --task-history-limit" -- "$cur" ) )
;;
esac
}
@ -1673,9 +1821,9 @@ _docker_node() {
accept
demote
inspect
ls
ls list
promote
rm
rm remove
tasks
update
"
@ -1701,20 +1849,46 @@ _docker_node_accept() {
esac
}
_docker_node_inspect() {
_docker_node_demote() {
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--help --format --pretty" -- "$cur" ) )
COMPREPLY=( $( compgen -W "--help" -- "$cur" ) )
;;
*)
__docker_complete_manager_nodes
esac
}
_docker_node_inspect() {
case "$prev" in
--format|-f)
return
;;
esac
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--format -f --help --pretty -p" -- "$cur" ) )
;;
*)
__docker_complete_nodes
esac
}
_docker_node_list() {
_docker_node_ls
}
_docker_node_ls() {
case "$prev" in
--filter|-f)
return
;;
esac
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--help --filter --quiet" -- "$cur" ) )
COMPREPLY=( $( compgen -W "--filter -f --help --quiet -q" -- "$cur" ) )
;;
esac
}
@ -1729,14 +1903,8 @@ _docker_node_promote() {
esac
}
_docker_node_demote() {
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--help" -- "$cur" ) )
;;
*)
__docker_complete_manager_nodes
esac
_docker_node_remove() {
_docker_node_rm
}
_docker_node_rm() {
@ -1750,9 +1918,15 @@ _docker_node_rm() {
}
_docker_node_tasks() {
case "$prev" in
--filter|-f)
return
;;
esac
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--help --no-resolve --filter --all" -- "$cur" ) )
COMPREPLY=( $( compgen -W "--all -a --filter -f --help --no-resolve -n" -- "$cur" ) )
;;
*)
__docker_complete_nodes_plus_self
@ -1760,9 +1934,24 @@ _docker_node_tasks() {
}
_docker_node_update() {
case "$prev" in
--availability)
COMPREPLY=( $( compgen -W "active drain pause" -- "$cur" ) )
return
;;
--membership)
COMPREPLY=( $( compgen -W "accepted rejected" -- "$cur" ) )
return
;;
--role)
COMPREPLY=( $( compgen -W "manager worker" -- "$cur" ) )
return
;;
esac
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--help --availability --membership --role" -- "$cur" ) )
COMPREPLY=( $( compgen -W "--availability --help --membership --role" -- "$cur" ) )
;;
*)
__docker_complete_nodes
@ -1970,7 +2159,7 @@ _docker_run() {
--cpu-quota
--cpuset-cpus
--cpuset-mems
--cpu-shares
--cpu-shares -c
--device
--device-read-bps
--device-read-iops
@ -1993,6 +2182,7 @@ _docker_run() {
--label-file
--label -l
--link
--link-local-ip
--log-driver
--log-opt
--mac-address
@ -2012,6 +2202,7 @@ _docker_run() {
--security-opt
--shm-size
--stop-signal
--storage-opt
--tmpfs
--sysctl
--ulimit
@ -2198,6 +2389,11 @@ _docker_run() {
fi
return
;;
--storage-opt)
COMPREPLY=( $( compgen -W "size" -S = -- "$cur") )
__docker_nospace
return
;;
--user|-u)
__docker_complete_user_group
return
@ -2365,7 +2561,7 @@ _docker_update() {
--cpu-quota
--cpuset-cpus
--cpuset-mems
--cpu-shares
--cpu-shares -c
--kernel-memory
--memory -m
--memory-reservation

View file

@ -415,8 +415,8 @@ __docker_complete_events_filter() {
(event)
local -a event_opts
event_opts=('attach' 'commit' 'connect' 'copy' 'create' 'delete' 'destroy' 'detach' 'die' 'disconnect' 'exec_create' 'exec_detach'
'exec_start' 'export' 'import' 'kill' 'mount' 'oom' 'pause' 'pull' 'push' 'reload' 'rename' 'resize' 'restart' 'start' 'stop' 'tag'
'top' 'unmount' 'unpause' 'untag' 'update')
'exec_start' 'export' 'import' 'kill' 'load' 'mount' 'oom' 'pause' 'pull' 'push' 'reload' 'rename' 'resize' 'restart' 'save' 'start'
'stop' 'tag' 'top' 'unmount' 'unpause' 'untag' 'update')
_describe -t event-filter-opts "event filter options" event_opts && ret=0
;;
(image)
@ -538,7 +538,7 @@ __docker_networks_names() {
__docker_network_commands() {
local -a _docker_network_subcommands
_docker_network_subcommands=(
"connect:Connects a container to a network"
"connect:Connect a container to a network"
"create:Creates a new network with a name specified by the user"
"disconnect:Disconnects a container from a network"
"inspect:Displays detailed information on a network"
@ -563,6 +563,7 @@ __docker_network_subcommand() {
"($help)--ip=[Container IPv4 address]:IPv4: " \
"($help)--ip6=[Container IPv6 address]:IPv6: " \
"($help)*--link=[Add a link to another container]:link:->link" \
"($help)*--link-local-ip=[Add a link-local address for the container]:IPv4/IPv6: " \
"($help -)1:network:__docker_networks" \
"($help -)2:containers:__docker_containers" && ret=0
@ -693,7 +694,7 @@ __docker_volume_commands() {
local -a _docker_volume_subcommands
_docker_volume_subcommands=(
"create:Create a volume"
"inspect:Return low-level information on a volume"
"inspect:Display detailed information on one or more volumes"
"ls:List volumes"
"rm:Remove a volume"
)
@ -786,7 +787,7 @@ __docker_subcommand() {
"($help)--userns=[Container user namespace]:user namespace:(host)"
)
opts_build_create_run_update=(
"($help)--cpu-shares=[CPU shares (relative weight)]:CPU shares:(0 10 100 200 500 800 1000)"
"($help -c --cpu-shares)"{-c=,--cpu-shares=}"[CPU shares (relative weight)]:CPU shares:(0 10 100 200 500 800 1000)"
"($help)--cpu-period=[Limit the CPU CFS (Completely Fair Scheduler) period]:CPU period: "
"($help)--cpu-quota=[Limit the CPU CFS (Completely Fair Scheduler) quota]:CPU quota: "
"($help)--cpuset-cpus=[CPUs in which to allow execution]:CPUs: "
@ -820,6 +821,7 @@ __docker_subcommand() {
"($help)--ip6=[Container IPv6 address]:IPv6: "
"($help)--ipc=[IPC namespace to use]:IPC namespace: "
"($help)*--link=[Add link to another container]:link:->link"
"($help)*--link-local-ip=[Add a link-local address for the container]:IPv4/IPv6: "
"($help)*"{-l=,--label=}"[Container metadata]:label: "
"($help)--log-driver=[Default driver for container logs]:Logging driver:(awslogs etwlogs fluentd gcplogs gelf journald json-file none splunk syslog)"
"($help)*--log-opt=[Log driver specific options]:log driver options:__docker_log_options"
@ -965,6 +967,7 @@ __docker_subcommand() {
"($help)--ipv6[Enable IPv6 networking]" \
"($help -l --log-level)"{-l=,--log-level=}"[Logging level]:level:(debug info warn error fatal)" \
"($help)*--label=[Key=value labels]:label: " \
"($help)--live-restore[Enable live restore of docker when containers are still running]" \
"($help)--log-driver=[Default driver for container logs]:Logging driver:(awslogs etwlogs fluentd gcplogs gelf journald json-file none splunk syslog)" \
"($help)*--log-opt=[Log driver specific options]:log driver options:__docker_log_options" \
"($help)--max-concurrent-downloads[Set the max concurrent downloads for each pull]" \
@ -973,7 +976,7 @@ __docker_subcommand() {
"($help -p --pidfile)"{-p=,--pidfile=}"[Path to use for daemon PID file]:PID file:_files" \
"($help)--raw-logs[Full timestamps without ANSI coloring]" \
"($help)*--registry-mirror=[Preferred Docker registry mirror]:registry mirror: " \
"($help -s --storage-driver)"{-s=,--storage-driver=}"[Storage driver to use]:driver:(aufs devicemapper btrfs zfs overlay)" \
"($help -s --storage-driver)"{-s=,--storage-driver=}"[Storage driver to use]:driver:(aufs btrfs devicemapper overlay overlay2 vfs zfs)" \
"($help)--selinux-enabled[Enable selinux support]" \
"($help)*--storage-opt=[Storage driver options]:storage driver options: " \
"($help)--tls[Use TLS]" \
@ -1250,6 +1253,7 @@ __docker_subcommand() {
"($help)--rm[Remove intermediate containers when it exits]" \
"($help)--sig-proxy[Proxy all received signals to the process (non-TTY mode only)]" \
"($help)--stop-signal=[Signal to kill a container]:signal:_signals" \
"($help)--storage-opt=[Set storage driver options per container]:storage options:->storage-opt" \
"($help -): :__docker_images" \
"($help -):command: _command_names -e" \
"($help -)*::arguments: _normal" && ret=0
@ -1262,6 +1266,14 @@ __docker_subcommand() {
__docker_runningcontainers -qS ":" && ret=0
fi
;;
(storage-opt)
if compset -P "*="; then
_message "value" && ret=0
else
opts=('size')
_describe -t filter-opts "storage options" opts -qS "=" && ret=0
fi
;;
esac
;;

View file

@ -13,10 +13,12 @@ import (
"google.golang.org/grpc"
"github.com/Sirupsen/logrus"
"github.com/docker/distribution/digest"
"github.com/docker/docker/daemon/cluster/convert"
executorpkg "github.com/docker/docker/daemon/cluster/executor"
"github.com/docker/docker/daemon/cluster/executor/container"
"github.com/docker/docker/errors"
"github.com/docker/docker/opts"
"github.com/docker/docker/pkg/ioutils"
"github.com/docker/docker/runconfig"
apitypes "github.com/docker/engine-api/types"
@ -30,15 +32,13 @@ const swarmDirName = "swarm"
const controlSocket = "control.sock"
const swarmConnectTimeout = 20 * time.Second
const stateFile = "docker-state.json"
const defaultAddr = "0.0.0.0:2377"
const (
initialReconnectDelay = 100 * time.Millisecond
maxReconnectDelay = 30 * time.Second
)
// ErrNoManager is returned then a manager-only function is called on non-manager
var ErrNoManager = fmt.Errorf("This node is not participating as a Swarm manager")
// ErrNoSwarm is returned on leaving a cluster that was never initialized
var ErrNoSwarm = fmt.Errorf("This node is not part of Swarm")
@ -51,6 +51,26 @@ var ErrPendingSwarmExists = fmt.Errorf("This node is processing an existing join
// ErrSwarmJoinTimeoutReached is returned when cluster join could not complete before timeout was reached.
var ErrSwarmJoinTimeoutReached = fmt.Errorf("Timeout was reached before node was joined. Attempt to join the cluster will continue in the background. Use \"docker info\" command to see the current Swarm status of your node.")
// defaultSpec contains some sane defaults if cluster options are missing on init
var defaultSpec = types.Spec{
Raft: types.RaftConfig{
SnapshotInterval: 10000,
KeepOldSnapshots: 0,
LogEntriesForSlowFollowers: 500,
HeartbeatTick: 1,
ElectionTick: 3,
},
CAConfig: types.CAConfig{
NodeCertExpiry: 90 * 24 * time.Hour,
},
Dispatcher: types.DispatcherConfig{
HeartbeatPeriod: uint64((5 * time.Second).Nanoseconds()),
},
Orchestration: types.OrchestrationConfig{
TaskHistoryRetentionLimit: 10,
},
}
type state struct {
ListenAddr string
}
@ -62,22 +82,27 @@ type Config struct {
Backend executorpkg.Backend
}
// Cluster provides capabilities to pariticipate in a cluster as worker or a
// manager and a worker.
// Cluster provides capabilities to participate in a cluster as a worker or a
// manager.
type Cluster struct {
sync.RWMutex
root string
config Config
configEvent chan struct{} // todo: make this array and goroutine safe
node *swarmagent.Node
*node
root string
config Config
configEvent chan struct{} // todo: make this array and goroutine safe
listenAddr string
stop bool
err error
cancelDelay func()
}
type node struct {
*swarmagent.Node
done chan struct{}
ready bool
conn *grpc.ClientConn
client swarmapi.ControlClient
ready bool
listenAddr string
err error
reconnectDelay time.Duration
stop bool
cancelDelay func()
}
// New creates a new Cluster instance using provided config.
@ -87,13 +112,12 @@ func New(config Config) (*Cluster, error) {
return nil, err
}
c := &Cluster{
root: root,
config: config,
configEvent: make(chan struct{}, 10),
reconnectDelay: initialReconnectDelay,
root: root,
config: config,
configEvent: make(chan struct{}, 10),
}
dt, err := ioutil.ReadFile(filepath.Join(root, stateFile))
st, err := c.loadState()
if err != nil {
if os.IsNotExist(err) {
return c, nil
@ -101,12 +125,7 @@ func New(config Config) (*Cluster, error) {
return nil, err
}
var st state
if err := json.Unmarshal(dt, &st); err != nil {
return nil, err
}
n, ctx, err := c.startNewNode(false, st.ListenAddr, "", "", "", false)
n, err := c.startNewNode(false, st.ListenAddr, "", "", "", false)
if err != nil {
return nil, err
}
@ -115,15 +134,32 @@ func New(config Config) (*Cluster, error) {
case <-time.After(swarmConnectTimeout):
logrus.Errorf("swarm component could not be started before timeout was reached")
case <-n.Ready():
case <-ctx.Done():
case <-n.done:
return nil, fmt.Errorf("swarm component could not be started: %v", c.err)
}
if ctx.Err() != nil {
return nil, fmt.Errorf("swarm component could not be started")
}
go c.reconnectOnFailure(ctx)
go c.reconnectOnFailure(n)
return c, nil
}
func (c *Cluster) loadState() (*state, error) {
dt, err := ioutil.ReadFile(filepath.Join(c.root, stateFile))
if err != nil {
return nil, err
}
// missing certificate means no actual state to restore from
if _, err := os.Stat(filepath.Join(c.root, "certificates/swarm-node.crt")); err != nil {
if os.IsNotExist(err) {
c.clearState()
}
return nil, err
}
var st state
if err := json.Unmarshal(dt, &st); err != nil {
return nil, err
}
return &st, nil
}
func (c *Cluster) saveState() error {
dt, err := json.Marshal(state{ListenAddr: c.listenAddr})
if err != nil {
@ -132,20 +168,20 @@ func (c *Cluster) saveState() error {
return ioutils.AtomicWriteFile(filepath.Join(c.root, stateFile), dt, 0600)
}
func (c *Cluster) reconnectOnFailure(ctx context.Context) {
func (c *Cluster) reconnectOnFailure(n *node) {
for {
<-ctx.Done()
<-n.done
c.Lock()
if c.stop || c.node != nil {
c.Unlock()
return
}
c.reconnectDelay *= 2
if c.reconnectDelay > maxReconnectDelay {
c.reconnectDelay = maxReconnectDelay
n.reconnectDelay *= 2
if n.reconnectDelay > maxReconnectDelay {
n.reconnectDelay = maxReconnectDelay
}
logrus.Warnf("Restarting swarm in %.2f seconds", c.reconnectDelay.Seconds())
delayCtx, cancel := context.WithTimeout(context.Background(), c.reconnectDelay)
logrus.Warnf("Restarting swarm in %.2f seconds", n.reconnectDelay.Seconds())
delayCtx, cancel := context.WithTimeout(context.Background(), n.reconnectDelay)
c.cancelDelay = cancel
c.Unlock()
<-delayCtx.Done()
@ -158,22 +194,23 @@ func (c *Cluster) reconnectOnFailure(ctx context.Context) {
return
}
var err error
_, ctx, err = c.startNewNode(false, c.listenAddr, c.getRemoteAddress(), "", "", false)
n, err = c.startNewNode(false, c.listenAddr, c.getRemoteAddress(), "", "", false)
if err != nil {
c.err = err
ctx = delayCtx
close(n.done)
}
c.Unlock()
}
}
func (c *Cluster) startNewNode(forceNewCluster bool, listenAddr, joinAddr, secret, cahash string, ismanager bool) (*swarmagent.Node, context.Context, error) {
func (c *Cluster) startNewNode(forceNewCluster bool, listenAddr, joinAddr, secret, cahash string, ismanager bool) (*node, error) {
if err := c.config.Backend.IsSwarmCompatible(); err != nil {
return nil, nil, err
return nil, err
}
c.node = nil
c.cancelDelay = nil
node, err := swarmagent.NewNode(&swarmagent.NodeConfig{
c.stop = false
n, err := swarmagent.NewNode(&swarmagent.NodeConfig{
Hostname: c.config.Name,
ForceNewCluster: forceNewCluster,
ListenControlAPI: filepath.Join(c.root, controlSocket),
@ -188,88 +225,85 @@ func (c *Cluster) startNewNode(forceNewCluster bool, listenAddr, joinAddr, secre
IsManager: ismanager,
})
if err != nil {
return nil, nil, err
return nil, err
}
ctx, cancel := context.WithCancel(context.Background())
if err := node.Start(ctx); err != nil {
return nil, nil, err
ctx := context.Background()
if err := n.Start(ctx); err != nil {
return nil, err
}
node := &node{
Node: n,
done: make(chan struct{}),
reconnectDelay: initialReconnectDelay,
}
c.node = node
c.listenAddr = listenAddr
c.saveState()
c.config.Backend.SetClusterProvider(c)
go func() {
err := node.Err(ctx)
err := n.Err(ctx)
if err != nil {
logrus.Errorf("cluster exited with error: %v", err)
}
c.Lock()
c.conn = nil
c.client = nil
c.node = nil
c.ready = false
c.err = err
c.Unlock()
cancel()
close(node.done)
}()
go func() {
select {
case <-node.Ready():
case <-n.Ready():
c.Lock()
c.reconnectDelay = initialReconnectDelay
c.Unlock()
case <-ctx.Done():
}
if ctx.Err() == nil {
c.Lock()
c.ready = true
node.ready = true
c.err = nil
c.Unlock()
case <-ctx.Done():
}
c.configEvent <- struct{}{}
}()
go func() {
for conn := range node.ListenControlSocket(ctx) {
for conn := range n.ListenControlSocket(ctx) {
c.Lock()
if c.conn != conn {
c.client = swarmapi.NewControlClient(conn)
if node.conn != conn {
if conn == nil {
node.client = nil
} else {
node.client = swarmapi.NewControlClient(conn)
}
}
if c.conn != nil {
c.client = nil
}
c.conn = conn
node.conn = conn
c.Unlock()
c.configEvent <- struct{}{}
}
}()
return node, ctx, nil
return node, nil
}
// Init initializes new cluster from user provided request.
func (c *Cluster) Init(req types.InitRequest) (string, error) {
c.Lock()
if node := c.node; node != nil {
c.Unlock()
if !req.ForceNewCluster {
c.Unlock()
return "", errSwarmExists(node)
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
c.cancelReconnect()
if err := c.node.Stop(ctx); err != nil && !strings.Contains(err.Error(), "context canceled") {
if err := c.stopNode(); err != nil {
c.Unlock()
return "", err
}
c.Lock()
c.node = nil
c.conn = nil
c.ready = false
}
if err := validateAndSanitizeInitRequest(&req); err != nil {
c.Unlock()
return "", err
}
// todo: check current state existing
n, ctx, err := c.startNewNode(req.ForceNewCluster, req.ListenAddr, "", "", "", false)
n, err := c.startNewNode(req.ForceNewCluster, req.ListenAddr, "", "", "", false)
if err != nil {
c.Unlock()
return "", err
@ -278,23 +312,20 @@ func (c *Cluster) Init(req types.InitRequest) (string, error) {
select {
case <-n.Ready():
if err := initAcceptancePolicy(n, req.Spec.AcceptancePolicy); err != nil {
if err := initClusterSpec(n, req.Spec); err != nil {
return "", err
}
go c.reconnectOnFailure(ctx)
go c.reconnectOnFailure(n)
return n.NodeID(), nil
case <-ctx.Done():
case <-n.done:
c.RLock()
defer c.RUnlock()
if c.err != nil {
if !req.ForceNewCluster { // if failure on first attempt don't keep state
if err := c.clearState(); err != nil {
return "", err
}
if !req.ForceNewCluster { // if failure on first attempt don't keep state
if err := c.clearState(); err != nil {
return "", err
}
return "", c.err
}
return "", ctx.Err()
return "", c.err
}
}
@ -305,11 +336,12 @@ func (c *Cluster) Join(req types.JoinRequest) error {
c.Unlock()
return errSwarmExists(node)
}
// todo: check current state existing
if len(req.RemoteAddrs) == 0 {
return fmt.Errorf("at least 1 RemoteAddr is required to join")
if err := validateAndSanitizeJoinRequest(&req); err != nil {
c.Unlock()
return err
}
n, ctx, err := c.startNewNode(false, req.ListenAddr, req.RemoteAddrs[0], req.Secret, req.CACertHash, req.Manager)
// todo: check current state existing
n, err := c.startNewNode(false, req.ListenAddr, req.RemoteAddrs[0], req.Secret, req.CACertHash, req.Manager)
if err != nil {
c.Unlock()
return err
@ -326,28 +358,41 @@ func (c *Cluster) Join(req types.JoinRequest) error {
certificateRequested = nil
case <-time.After(swarmConnectTimeout):
// attempt to connect will continue in background, also reconnecting
go c.reconnectOnFailure(ctx)
go c.reconnectOnFailure(n)
return ErrSwarmJoinTimeoutReached
case <-n.Ready():
go c.reconnectOnFailure(ctx)
go c.reconnectOnFailure(n)
return nil
case <-ctx.Done():
case <-n.done:
c.RLock()
defer c.RUnlock()
if c.err != nil {
return c.err
}
return ctx.Err()
return c.err
}
}
}
func (c *Cluster) cancelReconnect() {
// stopNode is a helper that stops the active c.node and waits until it has
// shut down. Call while keeping the cluster lock.
func (c *Cluster) stopNode() error {
if c.node == nil {
return nil
}
c.stop = true
if c.cancelDelay != nil {
c.cancelDelay()
c.cancelDelay = nil
}
node := c.node
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
// TODO: can't hold lock on stop because it calls back to network
c.Unlock()
defer c.Lock()
if err := node.Stop(ctx); err != nil && !strings.Contains(err.Error(), "context canceled") {
return err
}
<-node.done
return nil
}
// Leave shuts down Cluster and removes current state.
@ -370,25 +415,22 @@ func (c *Cluster) Leave(force bool) error {
c.Unlock()
return fmt.Errorf(msg)
}
msg += fmt.Sprintf("Leaving cluster will leave you with %v managers out of %v. This means Raft quorum will be lost and your cluster will become inaccessible. ", reachable-1, reachable+unreachable)
msg += fmt.Sprintf("Leaving cluster will leave you with %v managers out of %v. This means Raft quorum will be lost and your cluster will become inaccessible. ", reachable-1, reachable+unreachable)
}
}
} else {
msg += "Doing so may lose the consenus of your cluster. "
msg += "Doing so may lose the consensus of your cluster. "
}
msg += "Only way to restore a cluster that has lost consensus is to reinitialize it with `--force-new-cluster`. Use `--force` to ignore this message."
c.Unlock()
return fmt.Errorf(msg)
}
c.cancelReconnect()
c.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := node.Stop(ctx); err != nil && !strings.Contains(err.Error(), "context canceled") {
if err := c.stopNode(); err != nil {
c.Unlock()
return err
}
c.Unlock()
if nodeID := node.NodeID(); nodeID != "" {
for _, id := range c.config.Backend.ListContainersForNode(nodeID) {
if err := c.config.Backend.ContainerRm(id, &apitypes.ContainerRmConfig{ForceRemove: true}); err != nil {
@ -396,11 +438,6 @@ func (c *Cluster) Leave(force bool) error {
}
}
}
c.Lock()
defer c.Unlock()
c.node = nil
c.conn = nil
c.ready = false
c.configEvent <- struct{}{}
// todo: cleanup optional?
if err := c.clearState(); err != nil {
@ -410,6 +447,7 @@ func (c *Cluster) Leave(force bool) error {
}
func (c *Cluster) clearState() error {
// todo: backup this data instead of removing?
if err := os.RemoveAll(c.root); err != nil {
return err
}
@ -425,13 +463,13 @@ func (c *Cluster) getRequestContext() context.Context { // TODO: not needed when
return ctx
}
// Inspect retrives the confuguration properties of managed swarm cluster.
// Inspect retrieves the configuration properties of a managed swarm cluster.
func (c *Cluster) Inspect() (types.Swarm, error) {
c.RLock()
defer c.RUnlock()
if !c.isActiveManager() {
return types.Swarm{}, ErrNoManager
return types.Swarm{}, c.errNoManager()
}
swarm, err := getSwarm(c.getRequestContext(), c.client)
@ -452,7 +490,7 @@ func (c *Cluster) Update(version uint64, spec types.Spec) error {
defer c.RUnlock()
if !c.isActiveManager() {
return ErrNoManager
return c.errNoManager()
}
swarm, err := getSwarm(c.getRequestContext(), c.client)
@ -478,32 +516,32 @@ func (c *Cluster) Update(version uint64, spec types.Spec) error {
return err
}
// IsManager returns true is Cluster is participating as a manager.
// IsManager returns true if Cluster is participating as a manager.
func (c *Cluster) IsManager() bool {
c.RLock()
defer c.RUnlock()
return c.isActiveManager()
}
// IsAgent returns true is Cluster is participating as a worker/agent.
// IsAgent returns true if Cluster is participating as a worker/agent.
func (c *Cluster) IsAgent() bool {
c.RLock()
defer c.RUnlock()
return c.ready
return c.node != nil && c.ready
}
// GetListenAddress returns the listening address for current maanger's
// GetListenAddress returns the listening address for current manager's
// consensus and dispatcher APIs.
func (c *Cluster) GetListenAddress() string {
c.RLock()
defer c.RUnlock()
if c.conn != nil {
if c.isActiveManager() {
return c.listenAddr
}
return ""
}
// GetRemoteAddress returns a known advertise address of a remote maanger if
// GetRemoteAddress returns a known advertise address of a remote manager if
// available.
// todo: change to array/connect with info
func (c *Cluster) GetRemoteAddress() string {
@ -552,7 +590,6 @@ func (c *Cluster) Info() types.Info {
if c.err != nil {
info.Error = c.err.Error()
}
if c.isActiveManager() {
info.ControlAvailable = true
if r, err := c.client.ListNodes(c.getRequestContext(), &swarmapi.ListNodesRequest{}); err == nil {
@ -581,7 +618,19 @@ func (c *Cluster) Info() types.Info {
// isActiveManager should not be called without a read lock
func (c *Cluster) isActiveManager() bool {
return c.conn != nil
return c.node != nil && c.conn != nil
}
// errNoManager returns error describing why manager commands can't be used.
// Call with read lock.
func (c *Cluster) errNoManager() error {
if c.node == nil {
return fmt.Errorf("This node is not a Swarm manager. Use \"docker swarm init\" or \"docker swarm join --manager\" to connect this node to Swarm and try again.")
}
if c.node.Manager() != nil {
return fmt.Errorf("This node is not a Swarm manager. Manager is being prepared or has trouble connecting to the cluster.")
}
return fmt.Errorf("This node is not a Swarm manager. Worker nodes can't be used to view or modify cluster state. Please run this command on a manager node or promote the current node to a manager.")
}
// GetServices returns all services of a managed swarm cluster.
@ -590,7 +639,7 @@ func (c *Cluster) GetServices(options apitypes.ServiceListOptions) ([]types.Serv
defer c.RUnlock()
if !c.isActiveManager() {
return nil, ErrNoManager
return nil, c.errNoManager()
}
filters, err := newListServicesFilters(options.Filter)
@ -614,12 +663,12 @@ func (c *Cluster) GetServices(options apitypes.ServiceListOptions) ([]types.Serv
}
// CreateService creates a new service in a managed swarm cluster.
func (c *Cluster) CreateService(s types.ServiceSpec) (string, error) {
func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (string, error) {
c.RLock()
defer c.RUnlock()
if !c.isActiveManager() {
return "", ErrNoManager
return "", c.errNoManager()
}
ctx := c.getRequestContext()
@ -633,6 +682,15 @@ func (c *Cluster) CreateService(s types.ServiceSpec) (string, error) {
if err != nil {
return "", err
}
if encodedAuth != "" {
ctnr := serviceSpec.Task.GetContainer()
if ctnr == nil {
return "", fmt.Errorf("service does not use container tasks")
}
ctnr.PullOptions = &swarmapi.ContainerSpec_PullOptions{RegistryAuth: encodedAuth}
}
r, err := c.client.CreateService(ctx, &swarmapi.CreateServiceRequest{Spec: &serviceSpec})
if err != nil {
return "", err
@ -641,13 +699,13 @@ func (c *Cluster) CreateService(s types.ServiceSpec) (string, error) {
return r.Service.ID, nil
}
// GetService returns a service based on a ID or name.
// GetService returns a service based on an ID or name.
func (c *Cluster) GetService(input string) (types.Service, error) {
c.RLock()
defer c.RUnlock()
if !c.isActiveManager() {
return types.Service{}, ErrNoManager
return types.Service{}, c.errNoManager()
}
service, err := getService(c.getRequestContext(), c.client, input)
@ -658,12 +716,12 @@ func (c *Cluster) GetService(input string) (types.Service, error) {
}
// UpdateService updates existing service to match new properties.
func (c *Cluster) UpdateService(serviceID string, version uint64, spec types.ServiceSpec) error {
func (c *Cluster) UpdateService(serviceID string, version uint64, spec types.ServiceSpec, encodedAuth string) error {
c.RLock()
defer c.RUnlock()
if !c.isActiveManager() {
return ErrNoManager
return c.errNoManager()
}
serviceSpec, err := convert.ServiceSpecToGRPC(spec)
@ -671,6 +729,26 @@ func (c *Cluster) UpdateService(serviceID string, version uint64, spec types.Ser
return err
}
if encodedAuth != "" {
ctnr := serviceSpec.Task.GetContainer()
if ctnr == nil {
return fmt.Errorf("service does not use container tasks")
}
ctnr.PullOptions = &swarmapi.ContainerSpec_PullOptions{RegistryAuth: encodedAuth}
} else {
// this is needed because if the encodedAuth isn't being updated then we
// shouldn't lose it, and continue to use the one that was already present
currentService, err := getService(c.getRequestContext(), c.client, serviceID)
if err != nil {
return err
}
ctnr := currentService.Spec.Task.GetContainer()
if ctnr == nil {
return fmt.Errorf("service does not use container tasks")
}
serviceSpec.Task.GetContainer().PullOptions = ctnr.PullOptions
}
_, err = c.client.UpdateService(
c.getRequestContext(),
&swarmapi.UpdateServiceRequest{
@ -690,7 +768,7 @@ func (c *Cluster) RemoveService(input string) error {
defer c.RUnlock()
if !c.isActiveManager() {
return ErrNoManager
return c.errNoManager()
}
service, err := getService(c.getRequestContext(), c.client, input)
@ -710,7 +788,7 @@ func (c *Cluster) GetNodes(options apitypes.NodeListOptions) ([]types.Node, erro
defer c.RUnlock()
if !c.isActiveManager() {
return nil, ErrNoManager
return nil, c.errNoManager()
}
filters, err := newListNodesFilters(options.Filter)
@ -732,13 +810,13 @@ func (c *Cluster) GetNodes(options apitypes.NodeListOptions) ([]types.Node, erro
return nodes, nil
}
// GetNode returns a node based on a ID or name.
// GetNode returns a node based on an ID or name.
func (c *Cluster) GetNode(input string) (types.Node, error) {
c.RLock()
defer c.RUnlock()
if !c.isActiveManager() {
return types.Node{}, ErrNoManager
return types.Node{}, c.errNoManager()
}
node, err := getNode(c.getRequestContext(), c.client, input)
@ -754,7 +832,7 @@ func (c *Cluster) UpdateNode(nodeID string, version uint64, spec types.NodeSpec)
defer c.RUnlock()
if !c.isActiveManager() {
return ErrNoManager
return c.errNoManager()
}
nodeSpec, err := convert.NodeSpecToGRPC(spec)
@ -781,7 +859,7 @@ func (c *Cluster) RemoveNode(input string) error {
defer c.RUnlock()
if !c.isActiveManager() {
return ErrNoManager
return c.errNoManager()
}
ctx := c.getRequestContext()
@ -803,7 +881,7 @@ func (c *Cluster) GetTasks(options apitypes.TaskListOptions) ([]types.Task, erro
defer c.RUnlock()
if !c.isActiveManager() {
return nil, ErrNoManager
return nil, c.errNoManager()
}
filters, err := newListTasksFilters(options.Filter)
@ -831,7 +909,7 @@ func (c *Cluster) GetTask(input string) (types.Task, error) {
defer c.RUnlock()
if !c.isActiveManager() {
return types.Task{}, ErrNoManager
return types.Task{}, c.errNoManager()
}
task, err := getTask(c.getRequestContext(), c.client, input)
@ -841,13 +919,13 @@ func (c *Cluster) GetTask(input string) (types.Task, error) {
return convert.TaskFromGRPC(*task), nil
}
// GetNetwork returns a cluster network by ID.
// GetNetwork returns a cluster network by an ID.
func (c *Cluster) GetNetwork(input string) (apitypes.NetworkResource, error) {
c.RLock()
defer c.RUnlock()
if !c.isActiveManager() {
return apitypes.NetworkResource{}, ErrNoManager
return apitypes.NetworkResource{}, c.errNoManager()
}
network, err := getNetwork(c.getRequestContext(), c.client, input)
@ -863,7 +941,7 @@ func (c *Cluster) GetNetworks() ([]apitypes.NetworkResource, error) {
defer c.RUnlock()
if !c.isActiveManager() {
return nil, ErrNoManager
return nil, c.errNoManager()
}
r, err := c.client.ListNetworks(c.getRequestContext(), &swarmapi.ListNetworksRequest{})
@ -886,7 +964,7 @@ func (c *Cluster) CreateNetwork(s apitypes.NetworkCreateRequest) (string, error)
defer c.RUnlock()
if !c.isActiveManager() {
return "", ErrNoManager
return "", c.errNoManager()
}
if runconfig.IsPreDefinedNetwork(s.Name) {
@ -909,7 +987,7 @@ func (c *Cluster) RemoveNetwork(input string) error {
defer c.RUnlock()
if !c.isActiveManager() {
return ErrNoManager
return c.errNoManager()
}
network, err := getNetwork(c.getRequestContext(), c.client, input)
@ -969,7 +1047,7 @@ func (c *Cluster) Cleanup() {
c.Unlock()
return
}
defer c.Unlock()
if c.isActiveManager() {
active, reachable, unreachable, err := c.managerStats()
if err == nil {
@ -979,18 +1057,7 @@ func (c *Cluster) Cleanup() {
}
}
}
c.cancelReconnect()
c.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := node.Stop(ctx); err != nil {
logrus.Errorf("error cleaning up cluster: %v", err)
}
c.Lock()
c.node = nil
c.ready = false
c.conn = nil
c.Unlock()
c.stopNode()
}
func (c *Cluster) managerStats() (current bool, reachable int, unreachable int, err error) {
@ -1015,14 +1082,84 @@ func (c *Cluster) managerStats() (current bool, reachable int, unreachable int,
return
}
func errSwarmExists(node *swarmagent.Node) error {
func validateAndSanitizeInitRequest(req *types.InitRequest) error {
var err error
req.ListenAddr, err = validateAddr(req.ListenAddr)
if err != nil {
return fmt.Errorf("invalid ListenAddr %q: %v", req.ListenAddr, err)
}
spec := &req.Spec
// provide sane defaults instead of erroring
if spec.Name == "" {
spec.Name = "default"
}
if spec.Raft.SnapshotInterval == 0 {
spec.Raft.SnapshotInterval = defaultSpec.Raft.SnapshotInterval
}
if spec.Raft.LogEntriesForSlowFollowers == 0 {
spec.Raft.LogEntriesForSlowFollowers = defaultSpec.Raft.LogEntriesForSlowFollowers
}
if spec.Raft.ElectionTick == 0 {
spec.Raft.ElectionTick = defaultSpec.Raft.ElectionTick
}
if spec.Raft.HeartbeatTick == 0 {
spec.Raft.HeartbeatTick = defaultSpec.Raft.HeartbeatTick
}
if spec.Dispatcher.HeartbeatPeriod == 0 {
spec.Dispatcher.HeartbeatPeriod = defaultSpec.Dispatcher.HeartbeatPeriod
}
if spec.CAConfig.NodeCertExpiry == 0 {
spec.CAConfig.NodeCertExpiry = defaultSpec.CAConfig.NodeCertExpiry
}
if spec.Orchestration.TaskHistoryRetentionLimit == 0 {
spec.Orchestration.TaskHistoryRetentionLimit = defaultSpec.Orchestration.TaskHistoryRetentionLimit
}
return nil
}
func validateAndSanitizeJoinRequest(req *types.JoinRequest) error {
var err error
req.ListenAddr, err = validateAddr(req.ListenAddr)
if err != nil {
return fmt.Errorf("invalid ListenAddr %q: %v", req.ListenAddr, err)
}
if len(req.RemoteAddrs) == 0 {
return fmt.Errorf("at least 1 RemoteAddr is required to join")
}
for i := range req.RemoteAddrs {
req.RemoteAddrs[i], err = validateAddr(req.RemoteAddrs[i])
if err != nil {
return fmt.Errorf("invalid remoteAddr %q: %v", req.RemoteAddrs[i], err)
}
}
if req.CACertHash != "" {
if _, err := digest.ParseDigest(req.CACertHash); err != nil {
return fmt.Errorf("invalid CACertHash %q, %v", req.CACertHash, err)
}
}
return nil
}
func validateAddr(addr string) (string, error) {
if addr == "" {
return addr, fmt.Errorf("invalid empty address")
}
newaddr, err := opts.ParseTCPAddr(addr, defaultAddr)
if err != nil {
return addr, nil
}
return strings.TrimPrefix(newaddr, "tcp://"), nil
}
func errSwarmExists(node *node) error {
if node.NodeMembership() != swarmapi.NodeMembershipAccepted {
return ErrPendingSwarmExists
}
return ErrSwarmExists
}
func initAcceptancePolicy(node *swarmagent.Node, acceptancePolicy types.AcceptancePolicy) error {
func initClusterSpec(node *node, spec types.Spec) error {
ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
for conn := range node.ListenControlSocket(ctx) {
if ctx.Err() != nil {
@ -1046,15 +1183,14 @@ func initAcceptancePolicy(node *swarmagent.Node, acceptancePolicy types.Acceptan
cluster = lcr.Clusters[0]
break
}
spec := &cluster.Spec
if err := convert.SwarmSpecUpdateAcceptancePolicy(spec, acceptancePolicy, nil); err != nil {
newspec, err := convert.SwarmSpecToGRPCandMerge(spec, &cluster.Spec)
if err != nil {
return fmt.Errorf("error updating cluster settings: %v", err)
}
_, err := client.UpdateCluster(ctx, &swarmapi.UpdateClusterRequest{
_, err = client.UpdateCluster(ctx, &swarmapi.UpdateClusterRequest{
ClusterID: cluster.ID,
ClusterVersion: &cluster.Meta.Version,
Spec: spec,
Spec: &newspec,
})
if err != nil {
return fmt.Errorf("error updating cluster settings: %v", err)

View file

@ -3,6 +3,7 @@ package convert
import (
"fmt"
"strings"
"time"
"golang.org/x/crypto/bcrypt"
@ -26,14 +27,22 @@ func SwarmFromGRPC(c swarmapi.Cluster) types.Swarm {
HeartbeatTick: c.Spec.Raft.HeartbeatTick,
ElectionTick: c.Spec.Raft.ElectionTick,
},
Dispatcher: types.DispatcherConfig{
HeartbeatPeriod: c.Spec.Dispatcher.HeartbeatPeriod,
},
},
}
heartbeatPeriod, _ := ptypes.Duration(c.Spec.Dispatcher.HeartbeatPeriod)
swarm.Spec.Dispatcher.HeartbeatPeriod = uint64(heartbeatPeriod)
swarm.Spec.CAConfig.NodeCertExpiry, _ = ptypes.Duration(c.Spec.CAConfig.NodeCertExpiry)
for _, ca := range c.Spec.CAConfig.ExternalCAs {
swarm.Spec.CAConfig.ExternalCAs = append(swarm.Spec.CAConfig.ExternalCAs, &types.ExternalCA{
Protocol: types.ExternalCAProtocol(strings.ToLower(ca.Protocol.String())),
URL: ca.URL,
Options: ca.Options,
})
}
// Meta
swarm.Version.Index = c.Meta.Version.Index
swarm.CreatedAt, _ = ptypes.Timestamp(c.Meta.CreatedAt)
@ -76,13 +85,25 @@ func SwarmSpecToGRPCandMerge(s types.Spec, existingSpec *swarmapi.ClusterSpec) (
ElectionTick: s.Raft.ElectionTick,
},
Dispatcher: swarmapi.DispatcherConfig{
HeartbeatPeriod: s.Dispatcher.HeartbeatPeriod,
HeartbeatPeriod: ptypes.DurationProto(time.Duration(s.Dispatcher.HeartbeatPeriod)),
},
CAConfig: swarmapi.CAConfig{
NodeCertExpiry: ptypes.DurationProto(s.CAConfig.NodeCertExpiry),
},
}
for _, ca := range s.CAConfig.ExternalCAs {
protocol, ok := swarmapi.ExternalCA_CAProtocol_value[strings.ToUpper(string(ca.Protocol))]
if !ok {
return swarmapi.ClusterSpec{}, fmt.Errorf("invalid protocol: %q", ca.Protocol)
}
spec.CAConfig.ExternalCAs = append(spec.CAConfig.ExternalCAs, &swarmapi.ExternalCA{
Protocol: swarmapi.ExternalCA_CAProtocol(protocol),
URL: ca.URL,
Options: ca.Options,
})
}
if err := SwarmSpecUpdateAcceptancePolicy(&spec, s.AcceptancePolicy, existingSpec); err != nil {
return swarmapi.ClusterSpec{}, err
}

View file

@ -38,8 +38,13 @@ func newContainerAdapter(b executorpkg.Backend, task *api.Task) (*containerAdapt
}
func (c *containerAdapter) pullImage(ctx context.Context) error {
spec := c.container.spec()
// if the image needs to be pulled, the auth config will be retrieved and updated
encodedAuthConfig := c.container.spec().RegistryAuth
var encodedAuthConfig string
if spec.PullOptions != nil {
encodedAuthConfig = spec.PullOptions.RegistryAuth
}
authConfig := &types.AuthConfig{}
if encodedAuthConfig != "" {
@ -64,7 +69,7 @@ func (c *containerAdapter) pullImage(ctx context.Context) error {
}
return err
}
// TOOD(stevvooe): Report this status somewhere.
// TODO(stevvooe): Report this status somewhere.
logrus.Debugln("pull progress", m)
}
// if the final stream object contained an error, return it
@ -120,7 +125,7 @@ func (c *containerAdapter) create(ctx context.Context, backend executorpkg.Backe
return err
}
// Docker daemon currently doesnt support multiple networks in container create
// Docker daemon currently doesn't support multiple networks in container create
// Connect to all other networks
nc := c.container.connectNetworkingConfig()
@ -157,7 +162,7 @@ func (c *containerAdapter) inspect(ctx context.Context) (types.ContainerJSON, er
// events issues a call to the events API and returns a channel with all
// events. The stream of events can be shutdown by cancelling the context.
//
// A chan struct{} is returned that will be closed if the event procressing
// A chan struct{} is returned that will be closed if the event processing
// fails and needs to be restarted.
func (c *containerAdapter) wait(ctx context.Context) error {
return c.backend.ContainerWaitWithContext(ctx, c.container.name())

View file

@ -3,11 +3,12 @@ package container
import (
"errors"
"fmt"
"log"
"net"
"strings"
"time"
"github.com/Sirupsen/logrus"
clustertypes "github.com/docker/docker/daemon/cluster/provider"
"github.com/docker/docker/reference"
"github.com/docker/engine-api/types"
@ -18,7 +19,7 @@ import (
)
const (
// Explictly use the kernel's default setting for CPU quota of 100ms.
// Explicitly use the kernel's default setting for CPU quota of 100ms.
// https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt
cpuQuotaPeriod = 100 * time.Millisecond
@ -116,8 +117,8 @@ func (c *containerConfig) config() *enginecontainer.Config {
// If Command is provided, we replace the whole invocation with Command
// by replacing Entrypoint and specifying Cmd. Args is ignored in this
// case.
config.Entrypoint = append(config.Entrypoint, c.spec().Command[0])
config.Cmd = append(config.Cmd, c.spec().Command[1:]...)
config.Entrypoint = append(config.Entrypoint, c.spec().Command...)
config.Cmd = append(config.Cmd, c.spec().Args...)
} else if len(c.spec().Args) > 0 {
// In this case, we assume the image has an Entrypoint and Args
// specifies the arguments for that entrypoint.
@ -345,7 +346,7 @@ func (c *containerConfig) serviceConfig() *clustertypes.ServiceConfig {
return nil
}
log.Printf("Creating service config in agent for t = %+v", c.task)
logrus.Debugf("Creating service config in agent for t = %+v", c.task)
svcCfg := &clustertypes.ServiceConfig{
Name: c.task.ServiceAnnotations.Name,
Aliases: make(map[string][]string),
@ -403,6 +404,9 @@ func (c *containerConfig) networkCreateRequest(name string) (clustertypes.Networ
Driver: na.Network.IPAM.Driver.Name,
},
Options: na.Network.DriverState.Options,
Labels: na.Network.Spec.Annotations.Labels,
Internal: na.Network.Spec.Internal,
EnableIPv6: na.Network.Spec.Ipv6Enabled,
CheckDuplicate: true,
}

View file

@ -59,7 +59,6 @@ func (r *controller) ContainerStatus(ctx context.Context) (*api.ContainerStatus,
// Update tasks a recent task update and applies it to the container.
func (r *controller) Update(ctx context.Context, t *api.Task) error {
log.G(ctx).Warnf("task updates not yet supported")
// TODO(stevvooe): While assignment of tasks is idempotent, we do allow
// updates of metadata, such as labelling, as well as any other properties
// that make sense.
@ -152,9 +151,6 @@ func (r *controller) Wait(pctx context.Context) error {
defer cancel()
err := r.adapter.wait(ctx)
if err != nil {
return err
}
if ctx.Err() != nil {
return ctx.Err()
}
@ -166,6 +162,7 @@ func (r *controller) Wait(pctx context.Context) error {
if ec, ok := err.(exec.ExitCoder); ok {
ee.code = ec.ExitCode()
}
return ee
}
return nil
}

View file

@ -13,7 +13,7 @@ type NetworkCreateResponse struct {
ID string `json:"Id"`
}
// VirtualAddress represents a virtual adress.
// VirtualAddress represents a virtual address.
type VirtualAddress struct {
IPv4 string
IPv6 string

View file

@ -14,7 +14,6 @@ import (
"github.com/docker/docker/pkg/discovery"
flag "github.com/docker/docker/pkg/mflag"
"github.com/docker/docker/registry"
"github.com/docker/engine-api/types"
"github.com/imdario/mergo"
)
@ -27,6 +26,9 @@ const (
// maximum number of uploads that
// may take place at a time for each push.
defaultMaxConcurrentUploads = 5
// stockRuntimeName is the reserved name/alias used to represent the
// OCI runtime being shipped with the docker daemon package.
stockRuntimeName = "runc"
)
const (
@ -426,12 +428,12 @@ func ValidateConfiguration(config *Config) error {
// validate that "default" runtime is not reset
if runtimes := config.GetAllRuntimes(); len(runtimes) > 0 {
if _, ok := runtimes[types.DefaultRuntimeName]; ok {
return fmt.Errorf("runtime name '%s' is reserved", types.DefaultRuntimeName)
if _, ok := runtimes[stockRuntimeName]; ok {
return fmt.Errorf("runtime name '%s' is reserved", stockRuntimeName)
}
}
if defaultRuntime := config.GetDefaultRuntimeName(); defaultRuntime != "" && defaultRuntime != types.DefaultRuntimeName {
if defaultRuntime := config.GetDefaultRuntimeName(); defaultRuntime != "" && defaultRuntime != stockRuntimeName {
runtimes := config.GetAllRuntimes()
if _, ok := runtimes[defaultRuntime]; !ok {
return fmt.Errorf("specified default runtime '%s' does not exist", defaultRuntime)

View file

@ -88,8 +88,8 @@ func (config *Config) InstallFlags(cmd *flag.FlagSet, usageFn func(string) strin
cmd.StringVar(&config.ContainerdAddr, []string{"-containerd"}, "", usageFn("Path to containerd socket"))
cmd.BoolVar(&config.LiveRestore, []string{"-live-restore"}, false, usageFn("Enable live restore of docker when containers are still running"))
config.Runtimes = make(map[string]types.Runtime)
cmd.Var(runconfigopts.NewNamedRuntimeOpt("runtimes", &config.Runtimes), []string{"-add-runtime"}, usageFn("Register an additional OCI compatible runtime"))
cmd.StringVar(&config.DefaultRuntime, []string{"-default-runtime"}, types.DefaultRuntimeName, usageFn("Default OCI runtime to be used"))
cmd.Var(runconfigopts.NewNamedRuntimeOpt("runtimes", &config.Runtimes, stockRuntimeName), []string{"-add-runtime"}, usageFn("Register an additional OCI compatible runtime"))
cmd.StringVar(&config.DefaultRuntime, []string{"-default-runtime"}, stockRuntimeName, usageFn("Default OCI runtime to be used"))
config.attachExperimentalFlags(cmd, usageFn)
}
@ -123,7 +123,7 @@ func (config *Config) GetAllRuntimes() map[string]types.Runtime {
}
func (config *Config) isSwarmCompatible() error {
if config.IsValueSet("cluster-store") || config.IsValueSet("cluster-advertise") {
if config.ClusterStore != "" || config.ClusterAdvertise != "" {
return fmt.Errorf("--cluster-store and --cluster-advertise daemon configurations are incompatible with swarm mode")
}
if config.LiveRestore {

View file

@ -50,7 +50,7 @@ func (config *Config) GetRuntime(name string) *types.Runtime {
// GetDefaultRuntimeName returns the current default runtime
func (config *Config) GetDefaultRuntimeName() string {
return types.DefaultRuntimeName
return stockRuntimeName
}
// GetAllRuntimes returns a copy of the runtimes map

View file

@ -408,7 +408,21 @@ func (daemon *Daemon) allocateNetwork(container *container.Container) error {
updateSettings = true
}
// always connect default network first since only default
// network mode support link and we need do some setting
// on sandbox initialize for link, but the sandbox only be initialized
// on first network connecting.
defaultNetName := runconfig.DefaultDaemonNetworkMode().NetworkName()
if nConf, ok := container.NetworkSettings.Networks[defaultNetName]; ok {
if err := daemon.connectToNetwork(container, defaultNetName, nConf, updateSettings); err != nil {
return err
}
}
for n, nConf := range container.NetworkSettings.Networks {
if n == defaultNetName {
continue
}
if err := daemon.connectToNetwork(container, n, nConf, updateSettings); err != nil {
return err
}

View file

@ -385,6 +385,11 @@ func (daemon *Daemon) IsSwarmCompatible() error {
func NewDaemon(config *Config, registryService registry.Service, containerdRemote libcontainerd.Remote) (daemon *Daemon, err error) {
setDefaultMtu(config)
// Ensure that we have a correct root key limit for launching containers.
if err := ModifyRootKeyLimit(); err != nil {
logrus.Warnf("unable to modify root key limit, number of containers could be limitied by this quota: %v", err)
}
// Ensure we have compatible and valid configuration options
if err := verifyDaemonSettings(config); err != nil {
return nil, err

View file

@ -532,7 +532,7 @@ func (daemon *Daemon) platformReload(config *Config, attributes *map[string]stri
if config.IsValueSet("runtimes") {
daemon.configStore.Runtimes = config.Runtimes
// Always set the default one
daemon.configStore.Runtimes[types.DefaultRuntimeName] = types.Runtime{Path: DefaultRuntimeBinary}
daemon.configStore.Runtimes[stockRuntimeName] = types.Runtime{Path: DefaultRuntimeBinary}
}
if config.DefaultRuntime != "" {
@ -574,12 +574,12 @@ func verifyDaemonSettings(config *Config) error {
}
if config.DefaultRuntime == "" {
config.DefaultRuntime = types.DefaultRuntimeName
config.DefaultRuntime = stockRuntimeName
}
if config.Runtimes == nil {
config.Runtimes = make(map[string]types.Runtime)
}
config.Runtimes[types.DefaultRuntimeName] = types.Runtime{Path: DefaultRuntimeBinary}
config.Runtimes[stockRuntimeName] = types.Runtime{Path: DefaultRuntimeBinary}
return nil
}

View file

@ -197,6 +197,8 @@ func DriverTestDiffApply(t testing.TB, fileCount int, drivername string, driverO
defer PutDriver(t)
base := stringid.GenerateRandomID()
upper := stringid.GenerateRandomID()
deleteFile := "file-remove.txt"
deleteFileContent := []byte("This file should get removed in upper!")
if err := driver.Create(base, "", "", nil); err != nil {
t.Fatal(err)
@ -206,6 +208,10 @@ func DriverTestDiffApply(t testing.TB, fileCount int, drivername string, driverO
t.Fatal(err)
}
if err := addFile(driver, base, deleteFile, deleteFileContent); err != nil {
t.Fatal(err)
}
if err := driver.Create(upper, base, "", nil); err != nil {
t.Fatal(err)
}
@ -213,6 +219,11 @@ func DriverTestDiffApply(t testing.TB, fileCount int, drivername string, driverO
if err := addManyFiles(driver, upper, fileCount, 6); err != nil {
t.Fatal(err)
}
if err := removeFile(driver, upper, deleteFile); err != nil {
t.Fatal(err)
}
diffSize, err := driver.DiffSize(upper, "")
if err != nil {
t.Fatal(err)
@ -227,6 +238,10 @@ func DriverTestDiffApply(t testing.TB, fileCount int, drivername string, driverO
t.Fatal(err)
}
if err := checkFile(driver, diff, deleteFile, deleteFileContent); err != nil {
t.Fatal(err)
}
arch, err := driver.Diff(upper, base)
if err != nil {
t.Fatal(err)
@ -248,9 +263,14 @@ func DriverTestDiffApply(t testing.TB, fileCount int, drivername string, driverO
if applyDiffSize != diffSize {
t.Fatalf("Apply diff size different, got %d, expected %d", applyDiffSize, diffSize)
}
if err := checkManyFiles(driver, diff, fileCount, 6); err != nil {
t.Fatal(err)
}
if err := checkFileRemoved(driver, diff, deleteFile); err != nil {
t.Fatal(err)
}
}
// DriverTestChanges tests computed changes on a layer matches changes made

View file

@ -78,6 +78,32 @@ func addFile(drv graphdriver.Driver, layer, filename string, content []byte) err
return ioutil.WriteFile(path.Join(root, filename), content, 0755)
}
func removeFile(drv graphdriver.Driver, layer, filename string) error {
root, err := drv.Get(layer, "")
if err != nil {
return err
}
defer drv.Put(layer)
return os.Remove(path.Join(root, filename))
}
func checkFileRemoved(drv graphdriver.Driver, layer, filename string) error {
root, err := drv.Get(layer, "")
if err != nil {
return err
}
defer drv.Put(layer)
if _, err := os.Stat(path.Join(root, filename)); err == nil {
return fmt.Errorf("file still exists: %s", path.Join(root, filename))
} else if !os.IsNotExist(err) {
return err
}
return nil
}
func addManyFiles(drv graphdriver.Driver, layer string, count int, seed int64) error {
root, err := drv.Get(layer, "")
if err != nil {

View file

@ -32,12 +32,6 @@ type mountOptions struct {
}
func mountFrom(dir, device, target, mType, label string) error {
r, w, err := os.Pipe()
if err != nil {
return fmt.Errorf("mountfrom pipe failure: %v", err)
}
options := &mountOptions{
Device: device,
Target: target,
@ -47,7 +41,10 @@ func mountFrom(dir, device, target, mType, label string) error {
}
cmd := reexec.Command("docker-mountfrom", dir)
cmd.Stdin = r
w, err := cmd.StdinPipe()
if err != nil {
return fmt.Errorf("mountfrom error on pipe creation: %v", err)
}
output := bytes.NewBuffer(nil)
cmd.Stdout = output

View file

@ -99,6 +99,9 @@ func Init(base string, opt []string, uidMaps, gidMaps []idtools.IDMap) (graphdri
return nil, fmt.Errorf("BUG: zfs get all -t filesystem -rHp '%s' should contain '%s'", options.fsName, options.fsName)
}
if err := mount.MakePrivate(base); err != nil {
return nil, err
}
d := &Driver{
dataset: rootDataset,
options: options,

View file

@ -104,27 +104,34 @@ func (daemon *Daemon) ImageDelete(imageRef string, force, prune bool) ([]types.I
repoRefs = daemon.referenceStore.References(imgID)
// If this is a tag reference and all the remaining references
// to this image are digest references, delete the remaining
// references so that they don't prevent removal of the image.
// If a tag reference was removed and the only remaining
// references to the same repository are digest references,
// then clean up those digest references.
if _, isCanonical := parsedRef.(reference.Canonical); !isCanonical {
foundTagRef := false
foundRepoTagRef := false
for _, repoRef := range repoRefs {
if _, repoRefIsCanonical := repoRef.(reference.Canonical); !repoRefIsCanonical {
foundTagRef = true
if _, repoRefIsCanonical := repoRef.(reference.Canonical); !repoRefIsCanonical && parsedRef.Name() == repoRef.Name() {
foundRepoTagRef = true
break
}
}
if !foundTagRef {
if !foundRepoTagRef {
// Remove canonical references from same repository
remainingRefs := []reference.Named{}
for _, repoRef := range repoRefs {
if _, err := daemon.removeImageRef(repoRef); err != nil {
return records, err
}
if _, repoRefIsCanonical := repoRef.(reference.Canonical); repoRefIsCanonical && parsedRef.Name() == repoRef.Name() {
if _, err := daemon.removeImageRef(repoRef); err != nil {
return records, err
}
untaggedRecord := types.ImageDelete{Untagged: repoRef.String()}
records = append(records, untaggedRecord)
untaggedRecord := types.ImageDelete{Untagged: repoRef.String()}
records = append(records, untaggedRecord)
} else {
remainingRefs = append(remainingRefs, repoRef)
}
}
repoRefs = []reference.Named{}
repoRefs = remainingRefs
}
}
@ -135,11 +142,10 @@ func (daemon *Daemon) ImageDelete(imageRef string, force, prune bool) ([]types.I
removedRepositoryRef = true
} else {
// If an ID reference was given AND there is exactly one
// repository reference to the image then we will want to
// remove that reference.
// FIXME: Is this the behavior we want?
if len(repoRefs) == 1 {
// If an ID reference was given AND there is at most one tag
// reference to the image AND all references are within one
// repository, then remove all references.
if isSingleReference(repoRefs) {
c := conflictHard
if !force {
c |= conflictSoft &^ conflictActiveReference
@ -148,21 +154,48 @@ func (daemon *Daemon) ImageDelete(imageRef string, force, prune bool) ([]types.I
return nil, conflict
}
parsedRef, err := daemon.removeImageRef(repoRefs[0])
if err != nil {
return nil, err
for _, repoRef := range repoRefs {
parsedRef, err := daemon.removeImageRef(repoRef)
if err != nil {
return nil, err
}
untaggedRecord := types.ImageDelete{Untagged: parsedRef.String()}
daemon.LogImageEvent(imgID.String(), imgID.String(), "untag")
records = append(records, untaggedRecord)
}
untaggedRecord := types.ImageDelete{Untagged: parsedRef.String()}
daemon.LogImageEvent(imgID.String(), imgID.String(), "untag")
records = append(records, untaggedRecord)
}
}
return records, daemon.imageDeleteHelper(imgID, &records, force, prune, removedRepositoryRef)
}
// isSingleReference returns true when all references are from one repository
// and there is at most one tag. Returns false for empty input.
func isSingleReference(repoRefs []reference.Named) bool {
if len(repoRefs) <= 1 {
return len(repoRefs) == 1
}
var singleRef reference.Named
canonicalRefs := map[string]struct{}{}
for _, repoRef := range repoRefs {
if _, isCanonical := repoRef.(reference.Canonical); isCanonical {
canonicalRefs[repoRef.Name()] = struct{}{}
} else if singleRef == nil {
singleRef = repoRef
} else {
return false
}
}
if singleRef == nil {
// Just use first canonical ref
singleRef = repoRefs[0]
}
_, ok := canonicalRefs[singleRef.Name()]
return len(canonicalRefs) == 1 && ok
}
// isImageIDPrefix returns whether the given possiblePrefix is a prefix of the
// given imageID.
func isImageIDPrefix(imageID, possiblePrefix string) bool {

59
daemon/keys.go Normal file
View file

@ -0,0 +1,59 @@
// +build linux
package daemon
import (
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"
)
const (
rootKeyFile = "/proc/sys/kernel/keys/root_maxkeys"
rootBytesFile = "/proc/sys/kernel/keys/root_maxbytes"
rootKeyLimit = 1000000
// it is standard configuration to allocate 25 bytes per key
rootKeyByteMultiplier = 25
)
// ModifyRootKeyLimit checks to see if the root key limit is set to
// at least 1000000 and changes it to that limit along with the maxbytes
// allocated to the keys at a 25 to 1 multiplier.
func ModifyRootKeyLimit() error {
value, err := readRootKeyLimit(rootKeyFile)
if err != nil {
return err
}
if value < rootKeyLimit {
return setRootKeyLimit(rootKeyLimit)
}
return nil
}
func setRootKeyLimit(limit int) error {
keys, err := os.OpenFile(rootKeyFile, os.O_WRONLY, 0)
if err != nil {
return err
}
defer keys.Close()
if _, err := fmt.Fprintf(keys, "%d", limit); err != nil {
return err
}
bytes, err := os.OpenFile(rootBytesFile, os.O_WRONLY, 0)
if err != nil {
return err
}
defer bytes.Close()
_, err = fmt.Fprintf(bytes, "%d", limit*rootKeyByteMultiplier)
return err
}
func readRootKeyLimit(path string) (int, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return -1, err
}
return strconv.Atoi(strings.Trim(string(data), "\n"))
}

View file

@ -0,0 +1,8 @@
// +build !linux
package daemon
// ModifyRootKeyLimit is an noop on unsupported platforms.
func ModifyRootKeyLimit() error {
return nil
}

View file

@ -14,10 +14,11 @@ import (
// Writes are concurrent, so you need implement some sync in your logger
type Copier struct {
// srcs is map of name -> reader pairs, for example "stdout", "stderr"
srcs map[string]io.Reader
dst Logger
copyJobs sync.WaitGroup
closed chan struct{}
srcs map[string]io.Reader
dst Logger
copyJobs sync.WaitGroup
closeOnce sync.Once
closed chan struct{}
}
// NewCopier creates a new Copier
@ -74,9 +75,7 @@ func (c *Copier) Wait() {
// Close closes the copier
func (c *Copier) Close() {
select {
case <-c.closed:
default:
c.closeOnce.Do(func() {
close(c.closed)
}
})
}

View file

@ -77,6 +77,9 @@ func (l *JSONFileLogger) readLogs(logWatcher *logger.LogWatcher, config logger.R
}
if !config.Follow {
if err := latestFile.Close(); err != nil {
logrus.Errorf("Error closing file: %v", err)
}
return
}

View file

@ -11,6 +11,7 @@ import (
"errors"
"sort"
"strings"
"sync"
"time"
"github.com/docker/docker/pkg/jsonlog"
@ -83,6 +84,7 @@ type LogWatcher struct {
Msg chan *Message
// For sending error messages that occur while while reading logs.
Err chan error
closeOnce sync.Once
closeNotifier chan struct{}
}
@ -98,11 +100,9 @@ func NewLogWatcher() *LogWatcher {
// Close notifies the underlying log reader to stop.
func (w *LogWatcher) Close() {
// only close if not already closed
select {
case <-w.closeNotifier:
default:
w.closeOnce.Do(func() {
close(w.closeNotifier)
}
})
}
// WatchClose returns a channel receiver that receives notification

View file

@ -87,6 +87,13 @@ func (daemon *Daemon) ContainerLogs(ctx context.Context, containerName string, c
if !ok {
logrus.Debug("logs: end stream")
logs.Close()
if cLog != container.LogDriver {
// Since the logger isn't cached in the container, which occurs if it is running, it
// must get explicitly closed here to avoid leaking it and any file handles it has.
if err := cLog.Close(); err != nil {
logrus.Errorf("Error closing logger: %v", err)
}
}
return nil
}
logLine := msg.Line

View file

@ -152,7 +152,7 @@ func (daemon *Daemon) SetupIngress(create clustertypes.NetworkCreateRequest, nod
sb, err := controller.NewSandbox("ingress-sbox", libnetwork.OptionIngress())
if err != nil {
logrus.Errorf("Failed creating ingress sanbox: %v", err)
logrus.Errorf("Failed creating ingress sandbox: %v", err)
return
}

View file

@ -459,7 +459,7 @@ func setMounts(daemon *Daemon, s *specs.Spec, c *container.Container, mounts []c
userMounts[m.Destination] = struct{}{}
}
// Filter out mounts that are overriden by user supplied mounts
// Filter out mounts that are overridden by user supplied mounts
var defaultMounts []specs.Mount
_, mountDev := userMounts["/dev"]
for _, m := range s.Mounts {

View file

@ -21,6 +21,10 @@ func (daemon *Daemon) ContainerRename(oldName, newName string) error {
return fmt.Errorf("Neither old nor new names may be empty")
}
if newName[0] != '/' {
newName = "/" + newName
}
container, err := daemon.GetContainer(oldName)
if err != nil {
return err
@ -29,8 +33,13 @@ func (daemon *Daemon) ContainerRename(oldName, newName string) error {
oldName = container.Name
oldIsAnonymousEndpoint := container.NetworkSettings.IsAnonymousEndpoint
if oldName == newName {
return fmt.Errorf("Renaming a container with the same name as its current name")
}
container.Lock()
defer container.Unlock()
if newName, err = daemon.reserveName(container.ID, newName); err != nil {
return fmt.Errorf("Error when allocating new name: %v", err)
}

View file

@ -10,9 +10,15 @@ import (
func (daemon *Daemon) getLibcontainerdCreateOptions(container *container.Container) (*[]libcontainerd.CreateOption, error) {
createOptions := []libcontainerd.CreateOption{}
// Ensure a runtime has been assigned to this container
if container.HostConfig.Runtime == "" {
container.HostConfig.Runtime = stockRuntimeName
container.ToDisk()
}
rt := daemon.configStore.GetRuntime(container.HostConfig.Runtime)
if rt == nil {
return nil, fmt.Errorf("No such runtime '%s'", container.HostConfig.Runtime)
return nil, fmt.Errorf("no such runtime '%s'", container.HostConfig.Runtime)
}
createOptions = append(createOptions, libcontainerd.WithRuntime(rt.Path, rt.Args))

View file

@ -137,6 +137,7 @@ func (p *v2Pusher) pushV2Tag(ctx context.Context, ref reference.NamedTagged, ima
descriptorTemplate := v2PushDescriptor{
v2MetadataService: p.v2MetadataService,
repoInfo: p.repoInfo,
ref: p.ref,
repo: p.repo,
pushState: &p.pushState,
}
@ -222,13 +223,14 @@ type v2PushDescriptor struct {
layer layer.Layer
v2MetadataService *metadata.V2MetadataService
repoInfo reference.Named
ref reference.Named
repo distribution.Repository
pushState *pushState
remoteDescriptor distribution.Descriptor
}
func (pd *v2PushDescriptor) Key() string {
return "v2push:" + pd.repo.Named().Name() + " " + pd.layer.DiffID().String()
return "v2push:" + pd.ref.FullName() + " " + pd.layer.DiffID().String()
}
func (pd *v2PushDescriptor) ID() string {

View file

@ -15,13 +15,13 @@ Docker uses [Go templates](https://golang.org/pkg/text/template/) to allow users
of certain commands and log drivers. Each command a driver provides a detailed
list of elements they support in their templates:
- [Docker Images formatting](https://docs.docker.com/engine/reference/commandline/images/#formatting)
- [Docker Inspect formatting](https://docs.docker.com/engine/reference/commandline/inspect/#examples)
- [Docker Log Tag formatting](https://docs.docker.com/engine/admin/logging/log_tags/)
- [Docker Network Inspect formatting](https://docs.docker.com/engine/reference/commandline/network_inspect/)
- [Docker PS formatting](https://docs.docker.com/engine/reference/commandline/ps/#formatting)
- [Docker Volume Inspect formatting](https://docs.docker.com/engine/reference/commandline/volume_inspect/)
- [Docker Version formatting](https://docs.docker.com/engine/reference/commandline/version/#examples)
- [Docker Images formatting](../reference/commandline/images.md#formatting)
- [Docker Inspect formatting](../reference/commandline/inspect.md#examples)
- [Docker Log Tag formatting](logging/log_tags.md)
- [Docker Network Inspect formatting](../reference/commandline/network_inspect.md)
- [Docker PS formatting](../reference/commandline/ps.md#formatting)
- [Docker Volume Inspect formatting](../reference/commandline/volume_inspect.md)
- [Docker Version formatting](../reference/commandline/version.md#examples)
## Template functions

View file

@ -74,7 +74,7 @@ a new service that will be started after the docker daemon service has started.
ExecStop=/usr/bin/docker stop -t 2 redis_server
[Install]
WantedBy=local.target
WantedBy=default.target
If you need to pass options to the redis container (such as `--env`),
then you'll need to use `docker run` rather than `docker start`. This will

View file

@ -21,6 +21,17 @@ The following list of features are deprecated in Engine.
The `docker import` command format 'file|URL|- [REPOSITORY [TAG]]' is deprecated since November 2013. It's no more supported.
### `-h` shorthand for `--help`
**Deprecated In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)**
**Target For Removal In Release: [v1.14.0](https://github.com/docker/docker/releases/tag/v1.12.0)**
The shorthand (`-h`) is less common than `--help` on Linux and cannot be used
on all subcommands (due to it conflicting with, e.g. `-h` / `--hostname` on
`docker create`). For this reason, the `-h` shorthand was not printed in the
"usage" output of subcommands, nor docummented, and is now marked "deprecated".
### `-e` and `--email` flags on `docker login`
**Deprecated In Release: [v1.11.0](https://github.com/docker/docker/releases/tag/v1.11.0)**
@ -149,6 +160,16 @@ The following double-dash options are deprecated and have no replacement:
docker ps --before-id
docker search --trusted
**Deprecated In Release: [v1.5.0](https://github.com/docker/docker/releases/tag/v1.5.0)**
**Removed In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)**
The single-dash (`-help`) was removed, in favor of the double-dash `--help`
docker -help
docker [COMMAND] -help
### Interacting with V1 registries
Version 1.9 adds a flag (`--disable-legacy-registry=false`) which prevents the docker daemon from `pull`, `push`, and `login` operations against v1 registries. Though disabled by default, this signals the intent to deprecate the v1 protocol.

View file

@ -54,7 +54,10 @@ use:
$ docker logs -f test_apt_cacher_ng
To get your Debian-based containers to use the proxy, you have following options
To get your Debian-based containers to use the proxy, you have
following options. Note that you must replace `dockerhost` with the
IP address or FQDN of the host running the `test_apt_cacher_ng`
container.
1. Add an apt Proxy setting
`echo 'Acquire::http { Proxy "http://dockerhost:3142"; };' >> /etc/apt/conf.d/01proxy`

View file

@ -58,6 +58,7 @@ Plugin
[gce-docker plugin](https://github.com/mcuadros/gce-docker) | A volume plugin able to attach, format and mount Google Compute [persistent-disks](https://cloud.google.com/compute/docs/disks/persistent-disks).
[GlusterFS plugin](https://github.com/calavera/docker-volume-glusterfs) | A volume plugin that provides multi-host volumes management for Docker using GlusterFS.
[Horcrux Volume Plugin](https://github.com/muthu-r/horcrux) | A volume plugin that allows on-demand, version controlled access to your data. Horcrux is an open-source plugin, written in Go, and supports SCP, [Minio](https://www.minio.io) and Amazon S3.
[HPE 3Par Volume Plugin](https://github.com/hpe-storage/python-hpedockerplugin/) | A volume plugin that supports HPE 3Par and StoreVirtual iSCSI storage arrays.
[IPFS Volume Plugin](http://github.com/vdemeester/docker-volume-ipfs) | An open source volume plugin that allows using an [ipfs](https://ipfs.io/) filesystem as a volume.
[Keywhiz plugin](https://github.com/calavera/docker-volume-keywhiz) | A plugin that provides credentials and secret management using Keywhiz as a central repository.
[Local Persist Plugin](https://github.com/CWSpear/local-persist) | A volume plugin that extends the default `local` driver's functionality by allowing you specify a mountpoint anywhere on the host, which enables the files to *always persist*, even if the volume is removed via `docker volume rm`.
@ -67,7 +68,7 @@ Plugin
[Quobyte Volume Plugin](https://github.com/quobyte/docker-volume) | A volume plugin that connects Docker to [Quobyte](http://www.quobyte.com/containers)'s data center file system, a general-purpose scalable and fault-tolerant storage platform.
[REX-Ray plugin](https://github.com/emccode/rexray) | A volume plugin which is written in Go and provides advanced storage functionality for many platforms including VirtualBox, EC2, Google Compute Engine, OpenStack, and EMC.
[Virtuozzo Storage and Ploop plugin](https://github.com/virtuozzo/docker-volume-ploop) | A volume plugin with support for Virtuozzo Storage distributed cloud file system as well as ploop devices.
[VMware vSphere Storage Plugin](https://github.com/vmware/docker-volume-vsphere) | Docker Volume Driver for vSphere enables customers to address persistent storage requirements for Docker containers in vSphere environments.
[VMware vSphere Storage Plugin](https://github.com/vmware/docker-volume-vsphere) | Docker Volume Driver for vSphere enables customers to address persistent storage requirements for Docker containers in vSphere environments.
### Authorization plugins

View file

@ -51,7 +51,7 @@ respectively.
## Default user authorization mechanism
If TLS is enabled in the [Docker daemon](https://docs.docker.com/engine/security/https/), the default user authorization flow extracts the user details from the certificate subject name.
If TLS is enabled in the [Docker daemon](../security/https.md), the default user authorization flow extracts the user details from the certificate subject name.
That is, the `User` field is set to the client certificate subject common name, and the `AuthenticationMethod` field is set to `TLS`.
## Basic architecture

View file

@ -4,21 +4,37 @@ aliases = [
"/mac/started/",
"/windows/started/",
"/linux/started/",
"/getting-started/"
]
title = "Get Started with Docker"
description = "Getting started with Docker"
keywords = ["beginner, getting started, Docker"]
[menu.main]
identifier = "getstart_all"
parent = "engine_use"
weight="-80"
parent = "tutorial_getstart_menu"
weight="-1"
+++
<![end-metadata]-->
# Get Started with Docker
This tutorial is a for non-technical users who are interested in learning more about Docker. By following these steps, you'll learn fundamental Docker features while working through some simple tasks. You'll learn how to:
This tutorial is a for non-technical users who are interested in learning more about Docker. By following these steps, you'll learn fundamental Docker features while working through some simple tasks.
Depending on how you got here, you may or may not have already downloaded Docker for your platform and installed it.
## Got Docker?
If you haven't yet downloaded Docker for your platform or installed it, go to [Get Docker](step_one.md#step-1-get-docker).
## Ready to start working with Docker?
If you have already downloaded and installed Docker, you are ready to run Docker commands! Go to [Verify your installation](step_one.md#step-3-verify-your-installation).
### What you'll learn and do
You'll learn how to:
* install Docker software for your platform
* run a software image in a container
@ -30,10 +46,17 @@ This tutorial is a for non-technical users who are interested in learning more a
The getting started was user tested to reduce the chance of users having problems. For the best chance of success, follow the steps as written the first time before exploring on your own. It takes approximately 45 minutes to complete.
## Flavors of Docker
### Make sure you understand...
This tutorial is designed as a getting started with Docker, and works the same whether you are using Docker for Mac, Docker for Windows, Docker on Linux, or Docker Toolbox (for older Mac and Windows systems).
This getting started uses Docker Engine CLI commands entered on the command line of a terminal window. You don't need to be a wizard at the command line, but you should be familiar with how to open your favorite shell or terminal, and run basic commands in that environment. It helps (but isn't required) to know how to navigate a directory tree, manipulate files, list running process, and so forth.
If you are using Docker Toolbox, you can use the Docker Quickstart Terminal to run Docker commands in a pre-configured environment instead of opening a command line terminal.
If you are using Docker for Mac, Docker for Windows, or Docker on Linux, you will have Docker running in the background, and your standard command line terminal is already set up to run Docker commands.
## How much command line savvy do I need?
The getting started tour uses Docker Engine CLI commands entered on the command line of a terminal window. You don't need to be a wizard at the command line, but you should be familiar with how to open your favorite shell or terminal, and run basic commands in that environment. It helps (but isn't required) to know how to navigate a directory tree, manipulate files, list running process, and so forth.
Go to [the next page to install](step_one.md).

View file

@ -10,7 +10,7 @@ description = "Getting started with Docker"
keywords = ["beginner, getting started, Docker"]
[menu.main]
identifier = "getstart_learn_more"
parent = "getstart_all"
parent = "tutorial_getstart_menu"
weight = 7
+++
<![end-metadata]-->
@ -30,11 +30,11 @@ Depending on your interest, the Docker documentation contains a wealth of inform
</tr>
<tr>
<td class="tg-031e">More about Docker for Mac, features, examples, FAQs, relationship to Docker Machine and Docker Toolbox, and how this fits in the Docker ecosystem</td>
<td class="tg-031e">[Getting Started with Docker for Mac](/docker-for-mac/index.md)</td>
<td class="tg-031e">[Getting Started with Docker for Mac](https://docs.docker.com/docker-for-mac/)</td>
</tr>
<tr>
<td class="tg-031e">More about Docker for Windows, More about Docker for Windows, features, examples, FAQs, relationship to Docker Machine and Docker Toolbox, and how this fits in the Docker ecosystem</td>
<td class="tg-031e">[Getting Started with Docker for Windows](/docker-for-mac/index.md)</td>
<td class="tg-031e">More about Docker for Windows, features, examples, FAQs, relationship to Docker Machine and Docker Toolbox, and how this fits in the Docker ecosystem</td>
<td class="tg-031e">[Getting Started with Docker for Windows](https://docs.docker.com/docker-for-windows/)</td>
</tr>
<tr>
<td class="tg-031e">More about Docker Toolbox</td>

View file

@ -5,7 +5,7 @@ title = "Install Docker and run hello-world"
description = "Getting started with Docker"
keywords = ["beginner, getting started, Docker, install"]
identifier = "getstart_linux_install"
parent = "getstart_all"
parent = "tutorial_getstart_menu"
weight="-80"
+++
<![end-metadata]-->

16
docs/getstarted/menu.md Normal file
View file

@ -0,0 +1,16 @@
<!--[metadata]>
+++
aliases = [
]
title = "Get Started with Docker"
description = "Docker Mac"
keywords = ["beginner, getting started, Docker"]
type = "menu"
[menu.main]
identifier = "tutorial_getstart_menu"
parent = "engine_use"
weight = -80
+++
<![end-metadata]-->
# Get Started with Docker

View file

@ -10,7 +10,7 @@ description = "Getting started with Docker"
keywords = ["beginner, getting started, Docker"]
[menu.main]
identifier = "getstart_docker_hub"
parent = "getstart_all"
parent = "tutorial_getstart_menu"
weight = 5
+++
<![end-metadata]-->

View file

@ -10,7 +10,7 @@ description = "Getting started with Docker"
keywords = ["beginner, getting started, Docker"]
[menu.main]
identifier = "getstart_build_image"
parent = "getstart_all"
parent = "tutorial_getstart_menu"
weight = 4
+++
<![end-metadata]-->

View file

@ -10,22 +10,26 @@ description = "Getting started with Docker"
keywords = ["beginner, getting started, Docker, install"]
[menu.main]
identifier = "getstart_all_install"
parent = "getstart_all"
parent = "tutorial_getstart_menu"
weight = 1
+++
<![end-metadata]-->
# Install Docker
- [Step 1: Get Docker](#step-1-get-docker)
- [Step 2: Install Docker](#step-2-install-docker)
- [Step 3: Verify your installation](#step-3-verify-your-installation)
## Step 1: Get Docker
### Docker for Mac
Docker for Mac is our newest offering for the Mac. It runs as a native Mac application and uses <a href="https://github.com/mist64/xhyve/" target="_blank">xhyve</a> to virutalize the Docker Engine environment and Linux kernel-specific features for the Docker daemon.
Docker for Mac is our newest offering for the Mac. It runs as a native Mac application and uses <a href="https://github.com/mist64/xhyve/" target="_blank">xhyve</a> to virtualize the Docker Engine environment and Linux kernel-specific features for the Docker daemon.
<a class="button" href="https://download.docker.com/mac/beta/Docker.dmg">Get Docker for Mac</a>
#### Install Prequisites
**Requirements**
- Mac must be a 2010 or newer model, with Intel's hardware support for memory management unit (MMU) virtualization; i.e., Extended Page Tables (EPT)
@ -43,11 +47,11 @@ See [Docker Toolbox Overview](/toolbox/overview.md) for help on installing Docke
### Docker for Windows
Docker for Windows is our newest offering for PCs. It runs as a native Windows application and uses Hyper-V to virutalize the Docker Engine environment and Linux kernel-specific features for the Docker daemon.
Docker for Windows is our newest offering for PCs. It runs as a native Windows application and uses Hyper-V to virtualize the Docker Engine environment and Linux kernel-specific features for the Docker daemon.
<a class="button" href="https://download.docker.com/win/beta/InstallDocker.msi">Get Docker for Windows</a>
#### Install Prequisites
**Requirements**
* 64bit Windows 10 Pro, Enterprise and Education (1511 November update, Build 10586 or later). In the future we will support more versions of Windows 10.
@ -66,21 +70,19 @@ For full instructions on getting Docker for various Linux distributions, see [In
## Step 2: Install Docker
* For install instructions for Docker for Mac, see [Getting Started with Docker for Mac](/docker-for-mac/index.md).
- **Docker for Mac** - Install instructions are at [Getting Started with Docker for Mac](https://docs.docker.com/docker-for-mac/).
* For install instructions for Docker for Windows, see [Getting Started with Docker for Windows](/docker-for-windows/index.md).
- **Docker for Windows** - Install instructions are at [Getting Started with Docker for Windows](https://docs.docker.com/docker-for-windows/).
* For install instructions for Docker Toolbox, see [Docker Toolbox Overview](/toolbox/overview.md).
- **Docker Toolbox** - Install instructions are at [Docker Toolbox Overview](/toolbox/overview.md).
* For a simple example of installing Docker on Ubuntu Linux so that you can work through this tutorial, see [Installing Docker on Ubuntu Linux (Example)](linux_install_help.md).
For full install instructions for Docker on Linux, see [Install Docker Engine](/engine/installation/index.md) and select the flavor of Linux you want to use.
- **Docker on Linux** - For a simple example of installing Docker on Ubuntu Linux so that you can work through this tutorial, see [Installing Docker on Ubuntu Linux (Example)](linux_install_help.md). Full install instructions for all flavors of Linux we support are at [Install Docker Engine](/engine/installation/index.md).
## Step 3: Verify your installation
1. Open a command-line terminal, and run some Docker commands to verify that Docker is working as expected.
Some good commands to try are `docker version` to check that you have the latest release installed and `docker ps` to see if you have any running containers. (Probably not, since you just started.)
Some good commands to try are `docker version` to check that you have the latest release installed and `docker ps` to see if you have any running containers. (Probably not, since you just started.)
2. Type the `docker run hello-world` command and press RETURN.

View file

@ -10,7 +10,7 @@ description = "Getting started with Docker"
keywords = ["beginner, getting started, Docker"]
[menu.main]
identifier = "getstart_tag_push_pull"
parent = "getstart_all"
parent = "tutorial_getstart_menu"
weight = 6
+++
<![end-metadata]-->

Some files were not shown because too many files have changed in this diff Show more