X Tutup
Skip to content

Improve default handling for "service create"#32284

Merged
thaJeztah merged 2 commits intomoby:masterfrom
aaronlehmann:fix-service-defaults
Apr 11, 2017
Merged

Improve default handling for "service create"#32284
thaJeztah merged 2 commits intomoby:masterfrom
aaronlehmann:fix-service-defaults

Conversation

@aaronlehmann
Copy link

@aaronlehmann aaronlehmann commented Mar 31, 2017

Currently, the CLI hardcodes several defaults. This is a problem because it's awkward to keep CLI defaults in sync with swarmkit defaults. Also, when a service is being created, it generally takes on several default values of flags that the user did not specify. If these defaults are changed later in swarmkit, they will not apply to those services, because they will continue to use whatever the CLI hardcoded at the time.

This PR makes two changes across two commits.

  1. Changes docker service inspect to use the new InsertDefaults flag so that fields that are omitted in the service definition show default values in docker service inspect. This paves the way for docker service create to avoid hardcoding its defaults in services without hiding information from inspect.

  2. Changes docker service create to omit fields that are not specified by the user, when possible. This will allow defaults to be applied inside the manager. To provide visibility into defaults in the CLI help, the CLI uses swarmkit's api/defaults package to annotate docker service create --help output. These defaults are left out of docker service update --help, because defaults are misleading in this context - if a flag is not specified, it has no default action.

Fixes #24959
Fixes #32094

ping @aluzzardi @dongluochen @dnephin @thaJeztah

@vieux
Copy link
Contributor

vieux commented Apr 1, 2017

ping @dnephin

Copy link
Member

@dnephin dnephin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unfortunate that the pflag library tries to stay so close to the golang flag package API. It is not well suited for this type of behaviour.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is always passed the same value defaults.Service.Update.

Does this need to be a parameter? Could it look like the other defaults() functions and accept no params?

Also, this doesn't make any use of opts, which means it doesn't need to be an instance method. It should be just a function.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is passed either defaults.Service.Update or defaults.Rollback.Update. I agree that it's strange as a method on updateOpts. I'll change it to a function.

Any preference on whether it should be a single function that takes a *api.UpdateConfig (like this method), or separate functions that call a shared implementation with either the Update or Rollback default values?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I missed that, thanks!

This method moved to a function as-is sgtm.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't use r, so shouldn't be a method on restartPolicyOptions()

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With my suggested changes above this function will no longer need a serviceOpts, since these methods become functions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the pattern we normally use here is opts ServiceInspectOptions.

It's true that right now this would be a struct with a single field, but the next time we have to go adding a new flag we'll only need to change two lines, instead of every place ServiceInspectWithRaw() is called (for the second time).

Boolean paramaters are also hard to understand from the calling function, having insertDefaults: true is much nicer to read.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM. I noticed that there was a mix of styles, with some functions taking verbose, or force boolean arguments. I can switch to an options structure if that's the preferred style.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function became a lot harder to read. There's a lot of branching, and string formatting that's not really relevant to the primary responsibility of this function (adding flags).

Instead of a boolean argument, could we pass in a defaults map[string]string ?

All of this logic for formatting defaults can be moved to a buildServiceDefaultFlagMapping(), and passed as the argument from newCreateCommand(). In update we can pass an empty map[string]string{}.

With this new mapping all of the descriptions can become "shared text" + defaults[flagName].

I guess this is a little more complicated in the case where we are actually setting default values, not just error messages. I think that can be handled as well by creating a FlagDefaults struct (instead of just a map[string]string), and adding getInt64(string flagName) int64 accessors, along with another map for default values.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This formatting is duplicated quite a few times:

func formatDefault(value interface{})  string {
   ...
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does the help string claim a default that isn't actually set as the default on the flag?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DurationVar is a little weird. IIRC it shows the default value in the CLI help, but only if it's nonzero. I'd like us to always show the default on service create, even if it's zero, and never show it on service update. So it made sense to manipulate the flag description instead of providing a default value. If we did both, the default value would appear twice when the default is nonzero.

I can add a comment explaing this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would love to find a way to avoid needing flags here, but I can't find any right now.

Can we move this these back to the ToRestartPolicy() function, and add a flags arg ? (and the same with the few below this)

@aaronlehmann aaronlehmann force-pushed the fix-service-defaults branch 2 times, most recently from bf92ba3 to 6064953 Compare April 3, 2017 22:33
@aaronlehmann
Copy link
Author

@dnephin: Thanks for the thoughtful review. I've addressed the comments - PTAL

@aaronlehmann aaronlehmann force-pushed the fix-service-defaults branch from 6064953 to 1c5fb33 Compare April 4, 2017 02:14
@aaronlehmann
Copy link
Author

Rebased

@thaJeztah
Copy link
Member

I like this, finally time to give it a quick look (from a UX perspective), and seeing actual defaults in docker service create --help is really cool.

I haven't had time yet to test against an actually older daemon, but running this;

DOCKER_API_VERSION=1.26 docker service create --help

Seems to still show the defaults in docker service create --help; is that expected?

@aaronlehmann
Copy link
Author

I haven't had time yet to test against an actually older daemon, but running this;

DOCKER_API_VERSION=1.26 docker service create --help

Seems to still show the defaults in docker service create --help; is that expected?

Yes, it's expected. I've pondered the idea of adding an API endpoint where the client can query what defaults are in place on the manager. But it seems like a lot of complexity for something that is generally not a factor (defaults change rarely, and mixed client/server version setups are unusual, and the worst case outcome is outdated help text). In this implementation, the defaults shown in CLI help are linked into the client at build time.

If you think fetching the defaults via an API is worth it, we could consider going in that direction.

@aaronlehmann aaronlehmann force-pushed the fix-service-defaults branch from 1c5fb33 to 9d94390 Compare April 4, 2017 17:09
@aaronlehmann
Copy link
Author

Rebased

Copy link
Member

@dnephin dnephin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Design LGTM

@aaronlehmann aaronlehmann force-pushed the fix-service-defaults branch from 9d94390 to 17e9e60 Compare April 6, 2017 23:56
@aaronlehmann
Copy link
Author

Rebased

Copy link
Contributor

@dongluochen dongluochen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sgtm

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove (default 0s)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@aaronlehmann aaronlehmann force-pushed the fix-service-defaults branch from 17e9e60 to 1d6d4d7 Compare April 7, 2017 01:49
@dongluochen
Copy link
Contributor

design lgtm.

Copy link
Member

@vdemeester vdemeester left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Design LGTM, moving to code review

@vdemeester
Copy link
Member

@aaronlehmann needs a rebase 👼

@aaronlehmann aaronlehmann force-pushed the fix-service-defaults branch from 1d6d4d7 to bf47928 Compare April 7, 2017 17:35
@aaronlehmann
Copy link
Author

Rebased. flagUpdateOrder and flagRollbackOrder are now handled.

@vieux
Copy link
Contributor

vieux commented Apr 7, 2017

@aaronlehmann seems like the windows failure is related
..\..\api\server\router\swarm\helpers.go:42: not enough arguments in call to sr.backend.GetService

@aaronlehmann
Copy link
Author

Deleted my earlier comment, because it was indeed a problem with this PR. I thought it was an existing problem on master, but it turned out the reason it only showed up on windows was because each builder does a separate merge to master so they're not building the same version of the code (which is kind of a problem IMHO!).

@aaronlehmann aaronlehmann force-pushed the fix-service-defaults branch from bf47928 to d9331a2 Compare April 7, 2017 23:56
@aaronlehmann
Copy link
Author

@vieux: Fixed, thanks!

Copy link
Member

@vdemeester vdemeester left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 🐯

@thaJeztah
Copy link
Member

I'm a bit confused, but probably do something wrong..

docker service create --restart-condition="on-failure" --name web nginx:alpine
2i3pres421je9kuo5rh1vf2n4

Makes this request;

{
  "EndpointSpec": {
    "Mode": "vip"
  },
  "Labels": {},
  "Mode": {
    "Replicated": {}
  },
  "Name": "web",
  "TaskTemplate": {
    "ContainerSpec": {
      "DNSConfig": {},
      "Image": "nginx:alpine"
    },
    "ForceUpdate": 0,
    "Placement": {},
    "Resources": {
      "Limits": {},
      "Reservations": {}
    },
    "RestartPolicy": {
      "Condition": "on-failure",
      "Delay": 5000000000,
      "MaxAttempts": 0
    }
  }
}

With the insertDefaults=true option set;

/ # curl --unix-socket /var/run/docker.sock -sL http://localhost/v1.29/services/web?insertDefaults=true | jq .
{
  "ID": "2i3pres421je9kuo5rh1vf2n4",
  "Version": {
    "Index": 31
  },
  "CreatedAt": "2017-04-10T09:36:58.859857088Z",
  "UpdatedAt": "2017-04-10T09:36:58.859857088Z",
  "Spec": {
    "Name": "web",
    "Labels": {},
    "TaskTemplate": {
      "ContainerSpec": {
        "Image": "nginx:alpine@sha256:5aadb68304a38a8e2719605e4e180413f390cd6647602bee9bdedd59753c3590",
        "DNSConfig": {}
      },
      "Resources": {
        "Limits": {},
        "Reservations": {}
      },
      "RestartPolicy": {
        "Condition": "on-failure",
        "Delay": 5000000000,
        "MaxAttempts": 0
      },
      "Placement": {},
      "ForceUpdate": 0
    },
    "Mode": {
      "Replicated": {
        "Replicas": 1
      }
    },
    "EndpointSpec": {
      "Mode": "vip"
    }
  },
  "Endpoint": {
    "Spec": {}
  }
}

Without the insertDefaults=true option set;

/ # curl --unix-socket /var/run/docker.sock -sL http://localhost/v1.29/services/web | jq .
{
  "ID": "2i3pres421je9kuo5rh1vf2n4",
  "Version": {
    "Index": 31
  },
  "CreatedAt": "2017-04-10T09:36:58.859857088Z",
  "UpdatedAt": "2017-04-10T09:36:58.859857088Z",
  "Spec": {
    "Name": "web",
    "Labels": {},
    "TaskTemplate": {
      "ContainerSpec": {
        "Image": "nginx:alpine@sha256:5aadb68304a38a8e2719605e4e180413f390cd6647602bee9bdedd59753c3590",
        "DNSConfig": {}
      },
      "Resources": {
        "Limits": {},
        "Reservations": {}
      },
      "RestartPolicy": {
        "Condition": "on-failure",
        "Delay": 5000000000,
        "MaxAttempts": 0
      },
      "Placement": {},
      "ForceUpdate": 0
    },
    "Mode": {
      "Replicated": {
        "Replicas": 1
      }
    },
    "EndpointSpec": {
      "Mode": "vip"
    }
  },
  "Endpoint": {
    "Spec": {}
  }
}

@aaronlehmann aaronlehmann force-pushed the fix-service-defaults branch from d9331a2 to c115d68 Compare April 10, 2017 18:33
@aaronlehmann
Copy link
Author

@thaJeztah: You're right - there was a problem. Specifying the service ID led to the correct behavior, but services names were not handled properly, as the service was returned directly from ListServices. I've fixed this and added test coverage.

Aaron Lehmann added 2 commits April 10, 2017 13:41
This adds a new parameter insertDefaults to /services/{id}. When this is
set, an empty field (such as UpdateConfig) will be populated with
default values in the API response. Make "service inspect" use this, so
that empty fields do not result in missing information when inspecting a
service.

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
If no fields related to an update config or restart policy are
specified, these structs should not be created as part of the service,
to avoid hardcoding the current defaults.

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
@thaJeztah
Copy link
Member

Ah! That looks better;

docker service inspect web
[
    {
        "ID": "k850seipz5ohpemq1cotenze2",
        "Version": {
            "Index": 10
        },
        "CreatedAt": "2017-04-11T10:51:21.181483381Z",
        "UpdatedAt": "2017-04-11T10:51:21.181483381Z",
        "Spec": {
            "Name": "web",
            "Labels": {},
            "TaskTemplate": {
                "ContainerSpec": {
                    "Image": "nginx:alpine@sha256:5aadb68304a38a8e2719605e4e180413f390cd6647602bee9bdedd59753c3590",
                    "StopGracePeriod": 10000000000,
                    "DNSConfig": {}
                },
                "Resources": {
                    "Limits": {},
                    "Reservations": {}
                },
                "RestartPolicy": {
                    "Condition": "on-failure",
                    "Delay": 5000000000,
                    "MaxAttempts": 0
                },
                "Placement": {},
                "ForceUpdate": 0
            },
            "Mode": {
                "Replicated": {
                    "Replicas": 1
                }
            },
            "UpdateConfig": {
                "Parallelism": 1,
                "FailureAction": "pause",
                "Monitor": 5000000000,
                "MaxFailureRatio": 0,
                "Order": "stop-first"
            },
            "RollbackConfig": {
                "Parallelism": 1,
                "FailureAction": "pause",
                "Monitor": 5000000000,
                "MaxFailureRatio": 0,
                "Order": "stop-first"
            },
            "EndpointSpec": {
                "Mode": "vip"
            }
        },
        "Endpoint": {
            "Spec": {}
        }
    }
]

Copy link
Member

@thaJeztah thaJeztah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

description: "ID or name of service."
required: true
type: "string"
- name: "insertDefaults"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be added to the API version-history; https://github.com/docker/docker/blob/master/docs/api/version-history.md

(ok with doing that in a follow-up)

@thaJeztah
Copy link
Member

All green; I'll go ahead and merge so that this is in 17.05. I'll include the API version changes #32284 (comment) in the "changelog" pull request

@thaJeztah thaJeztah merged commit a258ef5 into moby:master Apr 11, 2017
@GordonTheTurtle GordonTheTurtle added this to the 17.05.0 milestone Apr 11, 2017
@aaronlehmann aaronlehmann deleted the fix-service-defaults branch April 12, 2017 01:06
dnephin pushed a commit to dnephin/docker that referenced this pull request Apr 17, 2017
Improve default handling for "service create"
dnephin pushed a commit to dnephin/docker that referenced this pull request Apr 17, 2017
Improve default handling for "service create"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

X Tutup