opts.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  1. package service
  2. import (
  3. "encoding/csv"
  4. "fmt"
  5. "math/big"
  6. "strconv"
  7. "strings"
  8. "time"
  9. "github.com/docker/docker/opts"
  10. runconfigopts "github.com/docker/docker/runconfig/opts"
  11. "github.com/docker/engine-api/types/swarm"
  12. "github.com/docker/go-connections/nat"
  13. units "github.com/docker/go-units"
  14. "github.com/spf13/cobra"
  15. )
  16. type int64Value interface {
  17. Value() int64
  18. }
  19. type memBytes int64
  20. func (m *memBytes) String() string {
  21. return units.BytesSize(float64(m.Value()))
  22. }
  23. func (m *memBytes) Set(value string) error {
  24. val, err := units.RAMInBytes(value)
  25. *m = memBytes(val)
  26. return err
  27. }
  28. func (m *memBytes) Type() string {
  29. return "MemoryBytes"
  30. }
  31. func (m *memBytes) Value() int64 {
  32. return int64(*m)
  33. }
  34. type nanoCPUs int64
  35. func (c *nanoCPUs) String() string {
  36. return big.NewRat(c.Value(), 1e9).FloatString(3)
  37. }
  38. func (c *nanoCPUs) Set(value string) error {
  39. cpu, ok := new(big.Rat).SetString(value)
  40. if !ok {
  41. return fmt.Errorf("Failed to parse %v as a rational number", value)
  42. }
  43. nano := cpu.Mul(cpu, big.NewRat(1e9, 1))
  44. if !nano.IsInt() {
  45. return fmt.Errorf("value is too precise")
  46. }
  47. *c = nanoCPUs(nano.Num().Int64())
  48. return nil
  49. }
  50. func (c *nanoCPUs) Type() string {
  51. return "NanoCPUs"
  52. }
  53. func (c *nanoCPUs) Value() int64 {
  54. return int64(*c)
  55. }
  56. // DurationOpt is an option type for time.Duration that uses a pointer. This
  57. // allows us to get nil values outside, instead of defaulting to 0
  58. type DurationOpt struct {
  59. value *time.Duration
  60. }
  61. // Set a new value on the option
  62. func (d *DurationOpt) Set(s string) error {
  63. v, err := time.ParseDuration(s)
  64. d.value = &v
  65. return err
  66. }
  67. // Type returns the type of this option
  68. func (d *DurationOpt) Type() string {
  69. return "duration-ptr"
  70. }
  71. // String returns a string repr of this option
  72. func (d *DurationOpt) String() string {
  73. if d.value != nil {
  74. return d.value.String()
  75. }
  76. return "none"
  77. }
  78. // Value returns the time.Duration
  79. func (d *DurationOpt) Value() *time.Duration {
  80. return d.value
  81. }
  82. // Uint64Opt represents a uint64.
  83. type Uint64Opt struct {
  84. value *uint64
  85. }
  86. // Set a new value on the option
  87. func (i *Uint64Opt) Set(s string) error {
  88. v, err := strconv.ParseUint(s, 0, 64)
  89. i.value = &v
  90. return err
  91. }
  92. // Type returns the type of this option
  93. func (i *Uint64Opt) Type() string {
  94. return "uint64-ptr"
  95. }
  96. // String returns a string repr of this option
  97. func (i *Uint64Opt) String() string {
  98. if i.value != nil {
  99. return fmt.Sprintf("%v", *i.value)
  100. }
  101. return "none"
  102. }
  103. // Value returns the uint64
  104. func (i *Uint64Opt) Value() *uint64 {
  105. return i.value
  106. }
  107. // MountOpt is a Value type for parsing mounts
  108. type MountOpt struct {
  109. values []swarm.Mount
  110. }
  111. // Set a new mount value
  112. func (m *MountOpt) Set(value string) error {
  113. csvReader := csv.NewReader(strings.NewReader(value))
  114. fields, err := csvReader.Read()
  115. if err != nil {
  116. return err
  117. }
  118. mount := swarm.Mount{}
  119. volumeOptions := func() *swarm.VolumeOptions {
  120. if mount.VolumeOptions == nil {
  121. mount.VolumeOptions = &swarm.VolumeOptions{
  122. Labels: make(map[string]string),
  123. }
  124. }
  125. if mount.VolumeOptions.DriverConfig == nil {
  126. mount.VolumeOptions.DriverConfig = &swarm.Driver{}
  127. }
  128. return mount.VolumeOptions
  129. }
  130. bindOptions := func() *swarm.BindOptions {
  131. if mount.BindOptions == nil {
  132. mount.BindOptions = new(swarm.BindOptions)
  133. }
  134. return mount.BindOptions
  135. }
  136. setValueOnMap := func(target map[string]string, value string) {
  137. parts := strings.SplitN(value, "=", 2)
  138. if len(parts) == 1 {
  139. target[value] = ""
  140. } else {
  141. target[parts[0]] = parts[1]
  142. }
  143. }
  144. mount.Type = swarm.MountTypeVolume // default to volume mounts
  145. // Set writable as the default
  146. for _, field := range fields {
  147. parts := strings.SplitN(field, "=", 2)
  148. key := strings.ToLower(parts[0])
  149. if len(parts) == 1 {
  150. switch key {
  151. case "readonly", "ro":
  152. mount.ReadOnly = true
  153. continue
  154. case "volume-nocopy":
  155. volumeOptions().NoCopy = true
  156. continue
  157. }
  158. }
  159. if len(parts) != 2 {
  160. return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
  161. }
  162. value := parts[1]
  163. switch key {
  164. case "type":
  165. mount.Type = swarm.MountType(strings.ToLower(value))
  166. case "source", "src":
  167. mount.Source = value
  168. case "target", "dst", "destination":
  169. mount.Target = value
  170. case "readonly", "ro":
  171. mount.ReadOnly, err = strconv.ParseBool(value)
  172. if err != nil {
  173. return fmt.Errorf("invalid value for %s: %s", key, value)
  174. }
  175. case "bind-propagation":
  176. bindOptions().Propagation = swarm.MountPropagation(strings.ToLower(value))
  177. case "volume-nocopy":
  178. volumeOptions().NoCopy, err = strconv.ParseBool(value)
  179. if err != nil {
  180. return fmt.Errorf("invalid value for populate: %s", value)
  181. }
  182. case "volume-label":
  183. setValueOnMap(volumeOptions().Labels, value)
  184. case "volume-driver":
  185. volumeOptions().DriverConfig.Name = value
  186. case "volume-opt":
  187. if volumeOptions().DriverConfig.Options == nil {
  188. volumeOptions().DriverConfig.Options = make(map[string]string)
  189. }
  190. setValueOnMap(volumeOptions().DriverConfig.Options, value)
  191. default:
  192. return fmt.Errorf("unexpected key '%s' in '%s'", key, field)
  193. }
  194. }
  195. if mount.Type == "" {
  196. return fmt.Errorf("type is required")
  197. }
  198. if mount.Target == "" {
  199. return fmt.Errorf("target is required")
  200. }
  201. if mount.VolumeOptions != nil && mount.Source == "" {
  202. return fmt.Errorf("source is required when specifying volume-* options")
  203. }
  204. if mount.Type == swarm.MountTypeBind && mount.VolumeOptions != nil {
  205. return fmt.Errorf("cannot mix 'volume-*' options with mount type '%s'", swarm.MountTypeBind)
  206. }
  207. if mount.Type == swarm.MountTypeVolume && mount.BindOptions != nil {
  208. return fmt.Errorf("cannot mix 'bind-*' options with mount type '%s'", swarm.MountTypeVolume)
  209. }
  210. m.values = append(m.values, mount)
  211. return nil
  212. }
  213. // Type returns the type of this option
  214. func (m *MountOpt) Type() string {
  215. return "mount"
  216. }
  217. // String returns a string repr of this option
  218. func (m *MountOpt) String() string {
  219. mounts := []string{}
  220. for _, mount := range m.values {
  221. repr := fmt.Sprintf("%s %s %s", mount.Type, mount.Source, mount.Target)
  222. mounts = append(mounts, repr)
  223. }
  224. return strings.Join(mounts, ", ")
  225. }
  226. // Value returns the mounts
  227. func (m *MountOpt) Value() []swarm.Mount {
  228. return m.values
  229. }
  230. type updateOptions struct {
  231. parallelism uint64
  232. delay time.Duration
  233. onFailure string
  234. }
  235. type resourceOptions struct {
  236. limitCPU nanoCPUs
  237. limitMemBytes memBytes
  238. resCPU nanoCPUs
  239. resMemBytes memBytes
  240. }
  241. func (r *resourceOptions) ToResourceRequirements() *swarm.ResourceRequirements {
  242. return &swarm.ResourceRequirements{
  243. Limits: &swarm.Resources{
  244. NanoCPUs: r.limitCPU.Value(),
  245. MemoryBytes: r.limitMemBytes.Value(),
  246. },
  247. Reservations: &swarm.Resources{
  248. NanoCPUs: r.resCPU.Value(),
  249. MemoryBytes: r.resMemBytes.Value(),
  250. },
  251. }
  252. }
  253. type restartPolicyOptions struct {
  254. condition string
  255. delay DurationOpt
  256. maxAttempts Uint64Opt
  257. window DurationOpt
  258. }
  259. func (r *restartPolicyOptions) ToRestartPolicy() *swarm.RestartPolicy {
  260. return &swarm.RestartPolicy{
  261. Condition: swarm.RestartPolicyCondition(r.condition),
  262. Delay: r.delay.Value(),
  263. MaxAttempts: r.maxAttempts.Value(),
  264. Window: r.window.Value(),
  265. }
  266. }
  267. func convertNetworks(networks []string) []swarm.NetworkAttachmentConfig {
  268. nets := []swarm.NetworkAttachmentConfig{}
  269. for _, network := range networks {
  270. nets = append(nets, swarm.NetworkAttachmentConfig{Target: network})
  271. }
  272. return nets
  273. }
  274. type endpointOptions struct {
  275. mode string
  276. ports opts.ListOpts
  277. }
  278. func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec {
  279. portConfigs := []swarm.PortConfig{}
  280. // We can ignore errors because the format was already validated by ValidatePort
  281. ports, portBindings, _ := nat.ParsePortSpecs(e.ports.GetAll())
  282. for port := range ports {
  283. portConfigs = append(portConfigs, convertPortToPortConfig(port, portBindings)...)
  284. }
  285. return &swarm.EndpointSpec{
  286. Mode: swarm.ResolutionMode(strings.ToLower(e.mode)),
  287. Ports: portConfigs,
  288. }
  289. }
  290. func convertPortToPortConfig(
  291. port nat.Port,
  292. portBindings map[nat.Port][]nat.PortBinding,
  293. ) []swarm.PortConfig {
  294. ports := []swarm.PortConfig{}
  295. for _, binding := range portBindings[port] {
  296. hostPort, _ := strconv.ParseUint(binding.HostPort, 10, 16)
  297. ports = append(ports, swarm.PortConfig{
  298. //TODO Name: ?
  299. Protocol: swarm.PortConfigProtocol(strings.ToLower(port.Proto())),
  300. TargetPort: uint32(port.Int()),
  301. PublishedPort: uint32(hostPort),
  302. })
  303. }
  304. return ports
  305. }
  306. type logDriverOptions struct {
  307. name string
  308. opts opts.ListOpts
  309. }
  310. func newLogDriverOptions() logDriverOptions {
  311. return logDriverOptions{opts: opts.NewListOpts(runconfigopts.ValidateEnv)}
  312. }
  313. func (ldo *logDriverOptions) toLogDriver() *swarm.Driver {
  314. if ldo.name == "" {
  315. return nil
  316. }
  317. // set the log driver only if specified.
  318. return &swarm.Driver{
  319. Name: ldo.name,
  320. Options: runconfigopts.ConvertKVStringsToMap(ldo.opts.GetAll()),
  321. }
  322. }
  323. // ValidatePort validates a string is in the expected format for a port definition
  324. func ValidatePort(value string) (string, error) {
  325. portMappings, err := nat.ParsePortSpec(value)
  326. for _, portMapping := range portMappings {
  327. if portMapping.Binding.HostIP != "" {
  328. return "", fmt.Errorf("HostIP is not supported by a service.")
  329. }
  330. }
  331. return value, err
  332. }
  333. type serviceOptions struct {
  334. name string
  335. labels opts.ListOpts
  336. containerLabels opts.ListOpts
  337. image string
  338. args []string
  339. env opts.ListOpts
  340. workdir string
  341. user string
  342. mounts MountOpt
  343. resources resourceOptions
  344. stopGrace DurationOpt
  345. replicas Uint64Opt
  346. mode string
  347. restartPolicy restartPolicyOptions
  348. constraints []string
  349. update updateOptions
  350. networks []string
  351. endpoint endpointOptions
  352. registryAuth bool
  353. logDriver logDriverOptions
  354. }
  355. func newServiceOptions() *serviceOptions {
  356. return &serviceOptions{
  357. labels: opts.NewListOpts(runconfigopts.ValidateEnv),
  358. containerLabels: opts.NewListOpts(runconfigopts.ValidateEnv),
  359. env: opts.NewListOpts(runconfigopts.ValidateEnv),
  360. endpoint: endpointOptions{
  361. ports: opts.NewListOpts(ValidatePort),
  362. },
  363. logDriver: newLogDriverOptions(),
  364. }
  365. }
  366. func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
  367. var service swarm.ServiceSpec
  368. service = swarm.ServiceSpec{
  369. Annotations: swarm.Annotations{
  370. Name: opts.name,
  371. Labels: runconfigopts.ConvertKVStringsToMap(opts.labels.GetAll()),
  372. },
  373. TaskTemplate: swarm.TaskSpec{
  374. ContainerSpec: swarm.ContainerSpec{
  375. Image: opts.image,
  376. Args: opts.args,
  377. Env: opts.env.GetAll(),
  378. Labels: runconfigopts.ConvertKVStringsToMap(opts.containerLabels.GetAll()),
  379. Dir: opts.workdir,
  380. User: opts.user,
  381. Mounts: opts.mounts.Value(),
  382. StopGracePeriod: opts.stopGrace.Value(),
  383. },
  384. Resources: opts.resources.ToResourceRequirements(),
  385. RestartPolicy: opts.restartPolicy.ToRestartPolicy(),
  386. Placement: &swarm.Placement{
  387. Constraints: opts.constraints,
  388. },
  389. LogDriver: opts.logDriver.toLogDriver(),
  390. },
  391. Mode: swarm.ServiceMode{},
  392. UpdateConfig: &swarm.UpdateConfig{
  393. Parallelism: opts.update.parallelism,
  394. Delay: opts.update.delay,
  395. FailureAction: opts.update.onFailure,
  396. },
  397. Networks: convertNetworks(opts.networks),
  398. EndpointSpec: opts.endpoint.ToEndpointSpec(),
  399. }
  400. switch opts.mode {
  401. case "global":
  402. if opts.replicas.Value() != nil {
  403. return service, fmt.Errorf("replicas can only be used with replicated mode")
  404. }
  405. service.Mode.Global = &swarm.GlobalService{}
  406. case "replicated":
  407. service.Mode.Replicated = &swarm.ReplicatedService{
  408. Replicas: opts.replicas.Value(),
  409. }
  410. default:
  411. return service, fmt.Errorf("Unknown mode: %s", opts.mode)
  412. }
  413. return service, nil
  414. }
  415. // addServiceFlags adds all flags that are common to both `create` and `update`.
  416. // Any flags that are not common are added separately in the individual command
  417. func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) {
  418. flags := cmd.Flags()
  419. flags.StringVar(&opts.name, flagName, "", "Service name")
  420. flags.StringVarP(&opts.workdir, flagWorkdir, "w", "", "Working directory inside the container")
  421. flags.StringVarP(&opts.user, flagUser, "u", "", "Username or UID (format: <name|uid>[:<group|gid>])")
  422. flags.Var(&opts.resources.limitCPU, flagLimitCPU, "Limit CPUs")
  423. flags.Var(&opts.resources.limitMemBytes, flagLimitMemory, "Limit Memory")
  424. flags.Var(&opts.resources.resCPU, flagReserveCPU, "Reserve CPUs")
  425. flags.Var(&opts.resources.resMemBytes, flagReserveMemory, "Reserve Memory")
  426. flags.Var(&opts.stopGrace, flagStopGracePeriod, "Time to wait before force killing a container")
  427. flags.Var(&opts.replicas, flagReplicas, "Number of tasks")
  428. flags.StringVar(&opts.restartPolicy.condition, flagRestartCondition, "", "Restart when condition is met (none, on-failure, or any)")
  429. flags.Var(&opts.restartPolicy.delay, flagRestartDelay, "Delay between restart attempts")
  430. flags.Var(&opts.restartPolicy.maxAttempts, flagRestartMaxAttempts, "Maximum number of restarts before giving up")
  431. flags.Var(&opts.restartPolicy.window, flagRestartWindow, "Window used to evaluate the restart policy")
  432. flags.Uint64Var(&opts.update.parallelism, flagUpdateParallelism, 1, "Maximum number of tasks updated simultaneously (0 to update all at once)")
  433. flags.DurationVar(&opts.update.delay, flagUpdateDelay, time.Duration(0), "Delay between updates")
  434. flags.StringVar(&opts.update.onFailure, flagUpdateFailureAction, "pause", "Action on update failure (pause|continue)")
  435. flags.StringVar(&opts.endpoint.mode, flagEndpointMode, "", "Endpoint mode (vip or dnsrr)")
  436. flags.BoolVar(&opts.registryAuth, flagRegistryAuth, false, "Send registry authentication details to swarm agents")
  437. flags.StringVar(&opts.logDriver.name, flagLogDriver, "", "Logging driver for service")
  438. flags.Var(&opts.logDriver.opts, flagLogOpt, "Logging driver options")
  439. }
  440. const (
  441. flagConstraint = "constraint"
  442. flagConstraintRemove = "constraint-rm"
  443. flagConstraintAdd = "constraint-add"
  444. flagContainerLabel = "container-label"
  445. flagContainerLabelRemove = "container-label-rm"
  446. flagContainerLabelAdd = "container-label-add"
  447. flagEndpointMode = "endpoint-mode"
  448. flagEnv = "env"
  449. flagEnvRemove = "env-rm"
  450. flagEnvAdd = "env-add"
  451. flagLabel = "label"
  452. flagLabelRemove = "label-rm"
  453. flagLabelAdd = "label-add"
  454. flagLimitCPU = "limit-cpu"
  455. flagLimitMemory = "limit-memory"
  456. flagMode = "mode"
  457. flagMount = "mount"
  458. flagMountRemove = "mount-rm"
  459. flagMountAdd = "mount-add"
  460. flagName = "name"
  461. flagNetwork = "network"
  462. flagNetworkRemove = "network-rm"
  463. flagNetworkAdd = "network-add"
  464. flagPublish = "publish"
  465. flagPublishRemove = "publish-rm"
  466. flagPublishAdd = "publish-add"
  467. flagReplicas = "replicas"
  468. flagReserveCPU = "reserve-cpu"
  469. flagReserveMemory = "reserve-memory"
  470. flagRestartCondition = "restart-condition"
  471. flagRestartDelay = "restart-delay"
  472. flagRestartMaxAttempts = "restart-max-attempts"
  473. flagRestartWindow = "restart-window"
  474. flagStopGracePeriod = "stop-grace-period"
  475. flagUpdateDelay = "update-delay"
  476. flagUpdateFailureAction = "update-failure-action"
  477. flagUpdateParallelism = "update-parallelism"
  478. flagUser = "user"
  479. flagWorkdir = "workdir"
  480. flagRegistryAuth = "with-registry-auth"
  481. flagLogDriver = "log-driver"
  482. flagLogOpt = "log-opt"
  483. )