opts.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. package service
  2. import (
  3. "encoding/csv"
  4. "fmt"
  5. "math/big"
  6. "strconv"
  7. "strings"
  8. "time"
  9. mounttypes "github.com/docker/docker/api/types/mount"
  10. "github.com/docker/docker/api/types/swarm"
  11. "github.com/docker/docker/opts"
  12. runconfigopts "github.com/docker/docker/runconfig/opts"
  13. "github.com/docker/go-connections/nat"
  14. units "github.com/docker/go-units"
  15. "github.com/spf13/cobra"
  16. )
  17. type int64Value interface {
  18. Value() int64
  19. }
  20. type memBytes int64
  21. func (m *memBytes) String() string {
  22. return units.BytesSize(float64(m.Value()))
  23. }
  24. func (m *memBytes) Set(value string) error {
  25. val, err := units.RAMInBytes(value)
  26. *m = memBytes(val)
  27. return err
  28. }
  29. func (m *memBytes) Type() string {
  30. return "MemoryBytes"
  31. }
  32. func (m *memBytes) Value() int64 {
  33. return int64(*m)
  34. }
  35. type nanoCPUs int64
  36. func (c *nanoCPUs) String() string {
  37. return big.NewRat(c.Value(), 1e9).FloatString(3)
  38. }
  39. func (c *nanoCPUs) Set(value string) error {
  40. cpu, ok := new(big.Rat).SetString(value)
  41. if !ok {
  42. return fmt.Errorf("Failed to parse %v as a rational number", value)
  43. }
  44. nano := cpu.Mul(cpu, big.NewRat(1e9, 1))
  45. if !nano.IsInt() {
  46. return fmt.Errorf("value is too precise")
  47. }
  48. *c = nanoCPUs(nano.Num().Int64())
  49. return nil
  50. }
  51. func (c *nanoCPUs) Type() string {
  52. return "NanoCPUs"
  53. }
  54. func (c *nanoCPUs) Value() int64 {
  55. return int64(*c)
  56. }
  57. // DurationOpt is an option type for time.Duration that uses a pointer. This
  58. // allows us to get nil values outside, instead of defaulting to 0
  59. type DurationOpt struct {
  60. value *time.Duration
  61. }
  62. // Set a new value on the option
  63. func (d *DurationOpt) Set(s string) error {
  64. v, err := time.ParseDuration(s)
  65. d.value = &v
  66. return err
  67. }
  68. // Type returns the type of this option
  69. func (d *DurationOpt) Type() string {
  70. return "duration-ptr"
  71. }
  72. // String returns a string repr of this option
  73. func (d *DurationOpt) String() string {
  74. if d.value != nil {
  75. return d.value.String()
  76. }
  77. return "none"
  78. }
  79. // Value returns the time.Duration
  80. func (d *DurationOpt) Value() *time.Duration {
  81. return d.value
  82. }
  83. // Uint64Opt represents a uint64.
  84. type Uint64Opt struct {
  85. value *uint64
  86. }
  87. // Set a new value on the option
  88. func (i *Uint64Opt) Set(s string) error {
  89. v, err := strconv.ParseUint(s, 0, 64)
  90. i.value = &v
  91. return err
  92. }
  93. // Type returns the type of this option
  94. func (i *Uint64Opt) Type() string {
  95. return "uint64-ptr"
  96. }
  97. // String returns a string repr of this option
  98. func (i *Uint64Opt) String() string {
  99. if i.value != nil {
  100. return fmt.Sprintf("%v", *i.value)
  101. }
  102. return "none"
  103. }
  104. // Value returns the uint64
  105. func (i *Uint64Opt) Value() *uint64 {
  106. return i.value
  107. }
  108. // MountOpt is a Value type for parsing mounts
  109. type MountOpt struct {
  110. values []mounttypes.Mount
  111. }
  112. // Set a new mount value
  113. func (m *MountOpt) Set(value string) error {
  114. csvReader := csv.NewReader(strings.NewReader(value))
  115. fields, err := csvReader.Read()
  116. if err != nil {
  117. return err
  118. }
  119. mount := mounttypes.Mount{}
  120. volumeOptions := func() *mounttypes.VolumeOptions {
  121. if mount.VolumeOptions == nil {
  122. mount.VolumeOptions = &mounttypes.VolumeOptions{
  123. Labels: make(map[string]string),
  124. }
  125. }
  126. if mount.VolumeOptions.DriverConfig == nil {
  127. mount.VolumeOptions.DriverConfig = &mounttypes.Driver{}
  128. }
  129. return mount.VolumeOptions
  130. }
  131. bindOptions := func() *mounttypes.BindOptions {
  132. if mount.BindOptions == nil {
  133. mount.BindOptions = new(mounttypes.BindOptions)
  134. }
  135. return mount.BindOptions
  136. }
  137. setValueOnMap := func(target map[string]string, value string) {
  138. parts := strings.SplitN(value, "=", 2)
  139. if len(parts) == 1 {
  140. target[value] = ""
  141. } else {
  142. target[parts[0]] = parts[1]
  143. }
  144. }
  145. mount.Type = mounttypes.TypeVolume // default to volume mounts
  146. // Set writable as the default
  147. for _, field := range fields {
  148. parts := strings.SplitN(field, "=", 2)
  149. key := strings.ToLower(parts[0])
  150. if len(parts) == 1 {
  151. switch key {
  152. case "readonly", "ro":
  153. mount.ReadOnly = true
  154. continue
  155. case "volume-nocopy":
  156. volumeOptions().NoCopy = true
  157. continue
  158. }
  159. }
  160. if len(parts) != 2 {
  161. return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
  162. }
  163. value := parts[1]
  164. switch key {
  165. case "type":
  166. mount.Type = mounttypes.Type(strings.ToLower(value))
  167. case "source", "src":
  168. mount.Source = value
  169. case "target", "dst", "destination":
  170. mount.Target = value
  171. case "readonly", "ro":
  172. mount.ReadOnly, err = strconv.ParseBool(value)
  173. if err != nil {
  174. return fmt.Errorf("invalid value for %s: %s", key, value)
  175. }
  176. case "bind-propagation":
  177. bindOptions().Propagation = mounttypes.Propagation(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.Type == mounttypes.TypeBind && mount.VolumeOptions != nil {
  203. return fmt.Errorf("cannot mix 'volume-*' options with mount type '%s'", mounttypes.TypeBind)
  204. }
  205. if mount.Type == mounttypes.TypeVolume && mount.BindOptions != nil {
  206. return fmt.Errorf("cannot mix 'bind-*' options with mount type '%s'", mounttypes.TypeVolume)
  207. }
  208. m.values = append(m.values, mount)
  209. return nil
  210. }
  211. // Type returns the type of this option
  212. func (m *MountOpt) Type() string {
  213. return "mount"
  214. }
  215. // String returns a string repr of this option
  216. func (m *MountOpt) String() string {
  217. mounts := []string{}
  218. for _, mount := range m.values {
  219. repr := fmt.Sprintf("%s %s %s", mount.Type, mount.Source, mount.Target)
  220. mounts = append(mounts, repr)
  221. }
  222. return strings.Join(mounts, ", ")
  223. }
  224. // Value returns the mounts
  225. func (m *MountOpt) Value() []mounttypes.Mount {
  226. return m.values
  227. }
  228. type updateOptions struct {
  229. parallelism uint64
  230. delay time.Duration
  231. monitor time.Duration
  232. onFailure string
  233. maxFailureRatio float32
  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. groups []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. Groups: opts.groups,
  383. Mounts: opts.mounts.Value(),
  384. StopGracePeriod: opts.stopGrace.Value(),
  385. },
  386. Networks: convertNetworks(opts.networks),
  387. Resources: opts.resources.ToResourceRequirements(),
  388. RestartPolicy: opts.restartPolicy.ToRestartPolicy(),
  389. Placement: &swarm.Placement{
  390. Constraints: opts.constraints,
  391. },
  392. LogDriver: opts.logDriver.toLogDriver(),
  393. },
  394. Networks: convertNetworks(opts.networks),
  395. Mode: swarm.ServiceMode{},
  396. UpdateConfig: &swarm.UpdateConfig{
  397. Parallelism: opts.update.parallelism,
  398. Delay: opts.update.delay,
  399. Monitor: opts.update.monitor,
  400. FailureAction: opts.update.onFailure,
  401. MaxFailureRatio: opts.update.maxFailureRatio,
  402. },
  403. EndpointSpec: opts.endpoint.ToEndpointSpec(),
  404. }
  405. switch opts.mode {
  406. case "global":
  407. if opts.replicas.Value() != nil {
  408. return service, fmt.Errorf("replicas can only be used with replicated mode")
  409. }
  410. service.Mode.Global = &swarm.GlobalService{}
  411. case "replicated":
  412. service.Mode.Replicated = &swarm.ReplicatedService{
  413. Replicas: opts.replicas.Value(),
  414. }
  415. default:
  416. return service, fmt.Errorf("Unknown mode: %s", opts.mode)
  417. }
  418. return service, nil
  419. }
  420. // addServiceFlags adds all flags that are common to both `create` and `update`.
  421. // Any flags that are not common are added separately in the individual command
  422. func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) {
  423. flags := cmd.Flags()
  424. flags.StringVarP(&opts.workdir, flagWorkdir, "w", "", "Working directory inside the container")
  425. flags.StringVarP(&opts.user, flagUser, "u", "", "Username or UID (format: <name|uid>[:<group|gid>])")
  426. flags.StringSliceVar(&opts.groups, flagGroupAdd, []string{}, "Add additional user groups to the container")
  427. flags.Var(&opts.resources.limitCPU, flagLimitCPU, "Limit CPUs")
  428. flags.Var(&opts.resources.limitMemBytes, flagLimitMemory, "Limit Memory")
  429. flags.Var(&opts.resources.resCPU, flagReserveCPU, "Reserve CPUs")
  430. flags.Var(&opts.resources.resMemBytes, flagReserveMemory, "Reserve Memory")
  431. flags.Var(&opts.stopGrace, flagStopGracePeriod, "Time to wait before force killing a container")
  432. flags.Var(&opts.replicas, flagReplicas, "Number of tasks")
  433. flags.StringVar(&opts.restartPolicy.condition, flagRestartCondition, "", "Restart when condition is met (none, on-failure, or any)")
  434. flags.Var(&opts.restartPolicy.delay, flagRestartDelay, "Delay between restart attempts")
  435. flags.Var(&opts.restartPolicy.maxAttempts, flagRestartMaxAttempts, "Maximum number of restarts before giving up")
  436. flags.Var(&opts.restartPolicy.window, flagRestartWindow, "Window used to evaluate the restart policy")
  437. flags.Uint64Var(&opts.update.parallelism, flagUpdateParallelism, 1, "Maximum number of tasks updated simultaneously (0 to update all at once)")
  438. flags.DurationVar(&opts.update.delay, flagUpdateDelay, time.Duration(0), "Delay between updates")
  439. flags.DurationVar(&opts.update.monitor, flagUpdateMonitor, time.Duration(0), "Duration after each task update to monitor for failure")
  440. flags.StringVar(&opts.update.onFailure, flagUpdateFailureAction, "pause", "Action on update failure (pause|continue)")
  441. flags.Float32Var(&opts.update.maxFailureRatio, flagUpdateMaxFailureRatio, 0, "Failure rate to tolerate during an update")
  442. flags.StringVar(&opts.endpoint.mode, flagEndpointMode, "", "Endpoint mode (vip or dnsrr)")
  443. flags.BoolVar(&opts.registryAuth, flagRegistryAuth, false, "Send registry authentication details to swarm agents")
  444. flags.StringVar(&opts.logDriver.name, flagLogDriver, "", "Logging driver for service")
  445. flags.Var(&opts.logDriver.opts, flagLogOpt, "Logging driver options")
  446. }
  447. const (
  448. flagConstraint = "constraint"
  449. flagConstraintRemove = "constraint-rm"
  450. flagConstraintAdd = "constraint-add"
  451. flagContainerLabel = "container-label"
  452. flagContainerLabelRemove = "container-label-rm"
  453. flagContainerLabelAdd = "container-label-add"
  454. flagEndpointMode = "endpoint-mode"
  455. flagEnv = "env"
  456. flagEnvRemove = "env-rm"
  457. flagEnvAdd = "env-add"
  458. flagGroupAdd = "group-add"
  459. flagGroupRemove = "group-rm"
  460. flagLabel = "label"
  461. flagLabelRemove = "label-rm"
  462. flagLabelAdd = "label-add"
  463. flagLimitCPU = "limit-cpu"
  464. flagLimitMemory = "limit-memory"
  465. flagMode = "mode"
  466. flagMount = "mount"
  467. flagMountRemove = "mount-rm"
  468. flagMountAdd = "mount-add"
  469. flagName = "name"
  470. flagNetwork = "network"
  471. flagPublish = "publish"
  472. flagPublishRemove = "publish-rm"
  473. flagPublishAdd = "publish-add"
  474. flagReplicas = "replicas"
  475. flagReserveCPU = "reserve-cpu"
  476. flagReserveMemory = "reserve-memory"
  477. flagRestartCondition = "restart-condition"
  478. flagRestartDelay = "restart-delay"
  479. flagRestartMaxAttempts = "restart-max-attempts"
  480. flagRestartWindow = "restart-window"
  481. flagStopGracePeriod = "stop-grace-period"
  482. flagUpdateDelay = "update-delay"
  483. flagUpdateFailureAction = "update-failure-action"
  484. flagUpdateMaxFailureRatio = "update-max-failure-ratio"
  485. flagUpdateMonitor = "update-monitor"
  486. flagUpdateParallelism = "update-parallelism"
  487. flagUser = "user"
  488. flagWorkdir = "workdir"
  489. flagRegistryAuth = "with-registry-auth"
  490. flagLogDriver = "log-driver"
  491. flagLogOpt = "log-opt"
  492. )