opts.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  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. if key == "readonly" || key == "ro" {
  151. mount.ReadOnly = true
  152. continue
  153. }
  154. if key == "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", "name", "src":
  167. mount.Source = value
  168. case "target", "dst", "dest", "destination", "path":
  169. mount.Target = value
  170. case "readonly", "ro":
  171. ro, err := strconv.ParseBool(value)
  172. if err != nil {
  173. return fmt.Errorf("invalid value for readonly: %s", value)
  174. }
  175. mount.ReadOnly = ro
  176. case "bind-propagation":
  177. bindOptions().Propagation = swarm.MountPropagation(strings.ToLower(value))
  178. case "volume-nocopy":
  179. volumeOptions().NoCopy, err = strconv.ParseBool(value)
  180. if err != nil {
  181. return fmt.Errorf("invalid value for populate: %s", value)
  182. }
  183. case "volume-label":
  184. setValueOnMap(volumeOptions().Labels, value)
  185. case "volume-driver":
  186. volumeOptions().DriverConfig.Name = value
  187. case "volume-opt":
  188. if volumeOptions().DriverConfig.Options == nil {
  189. volumeOptions().DriverConfig.Options = make(map[string]string)
  190. }
  191. setValueOnMap(volumeOptions().DriverConfig.Options, value)
  192. default:
  193. return fmt.Errorf("unexpected key '%s' in '%s'", key, field)
  194. }
  195. }
  196. if mount.Type == "" {
  197. return fmt.Errorf("type is required")
  198. }
  199. if mount.Target == "" {
  200. return fmt.Errorf("target is required")
  201. }
  202. if mount.VolumeOptions != nil && mount.Source == "" {
  203. return fmt.Errorf("source is required when specifying volume-* options")
  204. }
  205. if mount.Type == swarm.MountTypeBind && mount.VolumeOptions != nil {
  206. return fmt.Errorf("cannot mix 'volume-*' options with mount type '%s'", swarm.MountTypeBind)
  207. }
  208. if mount.Type == swarm.MountTypeVolume && mount.BindOptions != nil {
  209. return fmt.Errorf("cannot mix 'bind-*' options with mount type '%s'", swarm.MountTypeVolume)
  210. }
  211. m.values = append(m.values, mount)
  212. return nil
  213. }
  214. // Type returns the type of this option
  215. func (m *MountOpt) Type() string {
  216. return "mount"
  217. }
  218. // String returns a string repr of this option
  219. func (m *MountOpt) String() string {
  220. mounts := []string{}
  221. for _, mount := range m.values {
  222. repr := fmt.Sprintf("%s %s %s", mount.Type, mount.Source, mount.Target)
  223. mounts = append(mounts, repr)
  224. }
  225. return strings.Join(mounts, ", ")
  226. }
  227. // Value returns the mounts
  228. func (m *MountOpt) Value() []swarm.Mount {
  229. return m.values
  230. }
  231. type updateOptions struct {
  232. parallelism uint64
  233. delay time.Duration
  234. onFailure string
  235. }
  236. type resourceOptions struct {
  237. limitCPU nanoCPUs
  238. limitMemBytes memBytes
  239. resCPU nanoCPUs
  240. resMemBytes memBytes
  241. }
  242. func (r *resourceOptions) ToResourceRequirements() *swarm.ResourceRequirements {
  243. return &swarm.ResourceRequirements{
  244. Limits: &swarm.Resources{
  245. NanoCPUs: r.limitCPU.Value(),
  246. MemoryBytes: r.limitMemBytes.Value(),
  247. },
  248. Reservations: &swarm.Resources{
  249. NanoCPUs: r.resCPU.Value(),
  250. MemoryBytes: r.resMemBytes.Value(),
  251. },
  252. }
  253. }
  254. type restartPolicyOptions struct {
  255. condition string
  256. delay DurationOpt
  257. maxAttempts Uint64Opt
  258. window DurationOpt
  259. }
  260. func (r *restartPolicyOptions) ToRestartPolicy() *swarm.RestartPolicy {
  261. return &swarm.RestartPolicy{
  262. Condition: swarm.RestartPolicyCondition(r.condition),
  263. Delay: r.delay.Value(),
  264. MaxAttempts: r.maxAttempts.Value(),
  265. Window: r.window.Value(),
  266. }
  267. }
  268. func convertNetworks(networks []string) []swarm.NetworkAttachmentConfig {
  269. nets := []swarm.NetworkAttachmentConfig{}
  270. for _, network := range networks {
  271. nets = append(nets, swarm.NetworkAttachmentConfig{Target: network})
  272. }
  273. return nets
  274. }
  275. type endpointOptions struct {
  276. mode string
  277. ports opts.ListOpts
  278. }
  279. func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec {
  280. portConfigs := []swarm.PortConfig{}
  281. // We can ignore errors because the format was already validated by ValidatePort
  282. ports, portBindings, _ := nat.ParsePortSpecs(e.ports.GetAll())
  283. for port := range ports {
  284. portConfigs = append(portConfigs, convertPortToPortConfig(port, portBindings)...)
  285. }
  286. return &swarm.EndpointSpec{
  287. Mode: swarm.ResolutionMode(strings.ToLower(e.mode)),
  288. Ports: portConfigs,
  289. }
  290. }
  291. func convertPortToPortConfig(
  292. port nat.Port,
  293. portBindings map[nat.Port][]nat.PortBinding,
  294. ) []swarm.PortConfig {
  295. ports := []swarm.PortConfig{}
  296. for _, binding := range portBindings[port] {
  297. hostPort, _ := strconv.ParseUint(binding.HostPort, 10, 16)
  298. ports = append(ports, swarm.PortConfig{
  299. //TODO Name: ?
  300. Protocol: swarm.PortConfigProtocol(strings.ToLower(port.Proto())),
  301. TargetPort: uint32(port.Int()),
  302. PublishedPort: uint32(hostPort),
  303. })
  304. }
  305. return ports
  306. }
  307. type logDriverOptions struct {
  308. name string
  309. opts opts.ListOpts
  310. }
  311. func newLogDriverOptions() logDriverOptions {
  312. return logDriverOptions{opts: opts.NewListOpts(runconfigopts.ValidateEnv)}
  313. }
  314. func (ldo *logDriverOptions) toLogDriver() *swarm.Driver {
  315. if ldo.name == "" {
  316. return nil
  317. }
  318. // set the log driver only if specified.
  319. return &swarm.Driver{
  320. Name: ldo.name,
  321. Options: runconfigopts.ConvertKVStringsToMap(ldo.opts.GetAll()),
  322. }
  323. }
  324. // ValidatePort validates a string is in the expected format for a port definition
  325. func ValidatePort(value string) (string, error) {
  326. portMappings, err := nat.ParsePortSpec(value)
  327. for _, portMapping := range portMappings {
  328. if portMapping.Binding.HostIP != "" {
  329. return "", fmt.Errorf("HostIP is not supported by a service.")
  330. }
  331. }
  332. return value, err
  333. }
  334. type serviceOptions struct {
  335. name string
  336. labels opts.ListOpts
  337. containerLabels opts.ListOpts
  338. image string
  339. args []string
  340. env opts.ListOpts
  341. workdir string
  342. user string
  343. mounts MountOpt
  344. resources resourceOptions
  345. stopGrace DurationOpt
  346. replicas Uint64Opt
  347. mode string
  348. restartPolicy restartPolicyOptions
  349. constraints []string
  350. update updateOptions
  351. networks []string
  352. endpoint endpointOptions
  353. registryAuth bool
  354. logDriver logDriverOptions
  355. }
  356. func newServiceOptions() *serviceOptions {
  357. return &serviceOptions{
  358. labels: opts.NewListOpts(runconfigopts.ValidateEnv),
  359. containerLabels: opts.NewListOpts(runconfigopts.ValidateEnv),
  360. env: opts.NewListOpts(runconfigopts.ValidateEnv),
  361. endpoint: endpointOptions{
  362. ports: opts.NewListOpts(ValidatePort),
  363. },
  364. logDriver: newLogDriverOptions(),
  365. }
  366. }
  367. func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
  368. var service swarm.ServiceSpec
  369. service = swarm.ServiceSpec{
  370. Annotations: swarm.Annotations{
  371. Name: opts.name,
  372. Labels: runconfigopts.ConvertKVStringsToMap(opts.labels.GetAll()),
  373. },
  374. TaskTemplate: swarm.TaskSpec{
  375. ContainerSpec: swarm.ContainerSpec{
  376. Image: opts.image,
  377. Args: opts.args,
  378. Env: opts.env.GetAll(),
  379. Labels: runconfigopts.ConvertKVStringsToMap(opts.containerLabels.GetAll()),
  380. Dir: opts.workdir,
  381. User: opts.user,
  382. Mounts: opts.mounts.Value(),
  383. StopGracePeriod: opts.stopGrace.Value(),
  384. },
  385. Resources: opts.resources.ToResourceRequirements(),
  386. RestartPolicy: opts.restartPolicy.ToRestartPolicy(),
  387. Placement: &swarm.Placement{
  388. Constraints: opts.constraints,
  389. },
  390. LogDriver: opts.logDriver.toLogDriver(),
  391. },
  392. Mode: swarm.ServiceMode{},
  393. UpdateConfig: &swarm.UpdateConfig{
  394. Parallelism: opts.update.parallelism,
  395. Delay: opts.update.delay,
  396. FailureAction: opts.update.onFailure,
  397. },
  398. Networks: convertNetworks(opts.networks),
  399. EndpointSpec: opts.endpoint.ToEndpointSpec(),
  400. }
  401. switch opts.mode {
  402. case "global":
  403. if opts.replicas.Value() != nil {
  404. return service, fmt.Errorf("replicas can only be used with replicated mode")
  405. }
  406. service.Mode.Global = &swarm.GlobalService{}
  407. case "replicated":
  408. service.Mode.Replicated = &swarm.ReplicatedService{
  409. Replicas: opts.replicas.Value(),
  410. }
  411. default:
  412. return service, fmt.Errorf("Unknown mode: %s", opts.mode)
  413. }
  414. return service, nil
  415. }
  416. // addServiceFlags adds all flags that are common to both `create` and `update`.
  417. // Any flags that are not common are added separately in the individual command
  418. func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) {
  419. flags := cmd.Flags()
  420. flags.StringVar(&opts.name, flagName, "", "Service name")
  421. flags.StringVarP(&opts.workdir, "workdir", "w", "", "Working directory inside the container")
  422. flags.StringVarP(&opts.user, flagUser, "u", "", "Username or UID")
  423. flags.Var(&opts.resources.limitCPU, flagLimitCPU, "Limit CPUs")
  424. flags.Var(&opts.resources.limitMemBytes, flagLimitMemory, "Limit Memory")
  425. flags.Var(&opts.resources.resCPU, flagReserveCPU, "Reserve CPUs")
  426. flags.Var(&opts.resources.resMemBytes, flagReserveMemory, "Reserve Memory")
  427. flags.Var(&opts.stopGrace, flagStopGracePeriod, "Time to wait before force killing a container")
  428. flags.Var(&opts.replicas, flagReplicas, "Number of tasks")
  429. flags.StringVar(&opts.restartPolicy.condition, flagRestartCondition, "", "Restart when condition is met (none, on-failure, or any)")
  430. flags.Var(&opts.restartPolicy.delay, flagRestartDelay, "Delay between restart attempts")
  431. flags.Var(&opts.restartPolicy.maxAttempts, flagRestartMaxAttempts, "Maximum number of restarts before giving up")
  432. flags.Var(&opts.restartPolicy.window, flagRestartWindow, "Window used to evaluate the restart policy")
  433. flags.Uint64Var(&opts.update.parallelism, flagUpdateParallelism, 1, "Maximum number of tasks updated simultaneously (0 to update all at once)")
  434. flags.DurationVar(&opts.update.delay, flagUpdateDelay, time.Duration(0), "Delay between updates")
  435. flags.StringVar(&opts.update.onFailure, flagUpdateFailureAction, "pause", "Action on update failure (pause|continue)")
  436. flags.StringVar(&opts.endpoint.mode, flagEndpointMode, "", "Endpoint mode (vip or dnsrr)")
  437. flags.BoolVar(&opts.registryAuth, flagRegistryAuth, false, "Send registry authentication details to swarm agents")
  438. flags.StringVar(&opts.logDriver.name, flagLogDriver, "", "Logging driver for service")
  439. flags.Var(&opts.logDriver.opts, flagLogOpt, "Logging driver options")
  440. }
  441. const (
  442. flagConstraint = "constraint"
  443. flagConstraintRemove = "constraint-rm"
  444. flagConstraintAdd = "constraint-add"
  445. flagContainerLabel = "container-label"
  446. flagContainerLabelRemove = "container-label-rm"
  447. flagContainerLabelAdd = "container-label-add"
  448. flagEndpointMode = "endpoint-mode"
  449. flagEnv = "env"
  450. flagEnvRemove = "env-rm"
  451. flagEnvAdd = "env-add"
  452. flagLabel = "label"
  453. flagLabelRemove = "label-rm"
  454. flagLabelAdd = "label-add"
  455. flagLimitCPU = "limit-cpu"
  456. flagLimitMemory = "limit-memory"
  457. flagMode = "mode"
  458. flagMount = "mount"
  459. flagMountRemove = "mount-rm"
  460. flagMountAdd = "mount-add"
  461. flagName = "name"
  462. flagNetwork = "network"
  463. flagNetworkRemove = "network-rm"
  464. flagNetworkAdd = "network-add"
  465. flagPublish = "publish"
  466. flagPublishRemove = "publish-rm"
  467. flagPublishAdd = "publish-add"
  468. flagReplicas = "replicas"
  469. flagReserveCPU = "reserve-cpu"
  470. flagReserveMemory = "reserve-memory"
  471. flagRestartCondition = "restart-condition"
  472. flagRestartDelay = "restart-delay"
  473. flagRestartMaxAttempts = "restart-max-attempts"
  474. flagRestartWindow = "restart-window"
  475. flagStopGracePeriod = "stop-grace-period"
  476. flagUpdateDelay = "update-delay"
  477. flagUpdateFailureAction = "update-failure-action"
  478. flagUpdateParallelism = "update-parallelism"
  479. flagUser = "user"
  480. flagRegistryAuth = "with-registry-auth"
  481. flagLogDriver = "log-driver"
  482. flagLogOpt = "log-opt"
  483. )