opts.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  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. var (
  17. // DefaultReplicas is the default replicas to use for a replicated service
  18. DefaultReplicas uint64 = 1
  19. )
  20. type int64Value interface {
  21. Value() int64
  22. }
  23. type memBytes int64
  24. func (m *memBytes) String() string {
  25. return units.BytesSize(float64(m.Value()))
  26. }
  27. func (m *memBytes) Set(value string) error {
  28. val, err := units.RAMInBytes(value)
  29. *m = memBytes(val)
  30. return err
  31. }
  32. func (m *memBytes) Type() string {
  33. return "MemoryBytes"
  34. }
  35. func (m *memBytes) Value() int64 {
  36. return int64(*m)
  37. }
  38. type nanoCPUs int64
  39. func (c *nanoCPUs) String() string {
  40. return big.NewRat(c.Value(), 1e9).FloatString(3)
  41. }
  42. func (c *nanoCPUs) Set(value string) error {
  43. cpu, ok := new(big.Rat).SetString(value)
  44. if !ok {
  45. return fmt.Errorf("Failed to parse %v as a rational number", value)
  46. }
  47. nano := cpu.Mul(cpu, big.NewRat(1e9, 1))
  48. if !nano.IsInt() {
  49. return fmt.Errorf("value is too precise")
  50. }
  51. *c = nanoCPUs(nano.Num().Int64())
  52. return nil
  53. }
  54. func (c *nanoCPUs) Type() string {
  55. return "NanoCPUs"
  56. }
  57. func (c *nanoCPUs) Value() int64 {
  58. return int64(*c)
  59. }
  60. // DurationOpt is an option type for time.Duration that uses a pointer. This
  61. // allows us to get nil values outside, instead of defaulting to 0
  62. type DurationOpt struct {
  63. value *time.Duration
  64. }
  65. // Set a new value on the option
  66. func (d *DurationOpt) Set(s string) error {
  67. v, err := time.ParseDuration(s)
  68. d.value = &v
  69. return err
  70. }
  71. // Type returns the type of this option
  72. func (d *DurationOpt) Type() string {
  73. return "duration-ptr"
  74. }
  75. // String returns a string repr of this option
  76. func (d *DurationOpt) String() string {
  77. if d.value != nil {
  78. return d.value.String()
  79. }
  80. return "none"
  81. }
  82. // Value returns the time.Duration
  83. func (d *DurationOpt) Value() *time.Duration {
  84. return d.value
  85. }
  86. // Uint64Opt represents a uint64.
  87. type Uint64Opt struct {
  88. value *uint64
  89. }
  90. // Set a new value on the option
  91. func (i *Uint64Opt) Set(s string) error {
  92. v, err := strconv.ParseUint(s, 0, 64)
  93. i.value = &v
  94. return err
  95. }
  96. // Type returns the type of this option
  97. func (i *Uint64Opt) Type() string {
  98. return "uint64-ptr"
  99. }
  100. // String returns a string repr of this option
  101. func (i *Uint64Opt) String() string {
  102. if i.value != nil {
  103. return fmt.Sprintf("%v", *i.value)
  104. }
  105. return "none"
  106. }
  107. // Value returns the uint64
  108. func (i *Uint64Opt) Value() *uint64 {
  109. return i.value
  110. }
  111. // MountOpt is a Value type for parsing mounts
  112. type MountOpt struct {
  113. values []swarm.Mount
  114. }
  115. // Set a new mount value
  116. func (m *MountOpt) Set(value string) error {
  117. csvReader := csv.NewReader(strings.NewReader(value))
  118. fields, err := csvReader.Read()
  119. if err != nil {
  120. return err
  121. }
  122. mount := swarm.Mount{}
  123. volumeOptions := func() *swarm.VolumeOptions {
  124. if mount.VolumeOptions == nil {
  125. mount.VolumeOptions = &swarm.VolumeOptions{
  126. Labels: make(map[string]string),
  127. }
  128. }
  129. if mount.VolumeOptions.DriverConfig == nil {
  130. mount.VolumeOptions.DriverConfig = &swarm.Driver{}
  131. }
  132. return mount.VolumeOptions
  133. }
  134. bindOptions := func() *swarm.BindOptions {
  135. if mount.BindOptions == nil {
  136. mount.BindOptions = new(swarm.BindOptions)
  137. }
  138. return mount.BindOptions
  139. }
  140. setValueOnMap := func(target map[string]string, value string) {
  141. parts := strings.SplitN(value, "=", 2)
  142. if len(parts) == 1 {
  143. target[value] = ""
  144. } else {
  145. target[parts[0]] = parts[1]
  146. }
  147. }
  148. // Set writable as the default
  149. for _, field := range fields {
  150. parts := strings.SplitN(field, "=", 2)
  151. if len(parts) == 1 && strings.ToLower(parts[0]) == "readonly" {
  152. mount.ReadOnly = true
  153. continue
  154. }
  155. if len(parts) == 1 && strings.ToLower(parts[0]) == "volume-nocopy" {
  156. volumeOptions().NoCopy = true
  157. continue
  158. }
  159. if len(parts) != 2 {
  160. return fmt.Errorf("invalid field '%s' must be a key=value pair", field)
  161. }
  162. key, value := parts[0], parts[1]
  163. switch strings.ToLower(key) {
  164. case "type":
  165. mount.Type = swarm.MountType(strings.ToUpper(value))
  166. case "source":
  167. mount.Source = value
  168. case "target":
  169. mount.Target = value
  170. case "readonly":
  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.ToUpper(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.MountType("BIND") && mount.VolumeOptions != nil {
  206. return fmt.Errorf("cannot mix 'volume-*' options with mount type '%s'", swarm.MountTypeBind)
  207. }
  208. if mount.Type == swarm.MountType("VOLUME") && 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. }
  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. image string
  337. args []string
  338. env opts.ListOpts
  339. workdir string
  340. user string
  341. mounts MountOpt
  342. resources resourceOptions
  343. stopGrace DurationOpt
  344. replicas Uint64Opt
  345. mode string
  346. restartPolicy restartPolicyOptions
  347. constraints []string
  348. update updateOptions
  349. networks []string
  350. endpoint endpointOptions
  351. registryAuth bool
  352. logDriver logDriverOptions
  353. }
  354. func newServiceOptions() *serviceOptions {
  355. return &serviceOptions{
  356. labels: opts.NewListOpts(runconfigopts.ValidateEnv),
  357. env: opts.NewListOpts(runconfigopts.ValidateEnv),
  358. endpoint: endpointOptions{
  359. ports: opts.NewListOpts(ValidatePort),
  360. },
  361. logDriver: newLogDriverOptions(),
  362. }
  363. }
  364. func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) {
  365. var service swarm.ServiceSpec
  366. service = swarm.ServiceSpec{
  367. Annotations: swarm.Annotations{
  368. Name: opts.name,
  369. Labels: runconfigopts.ConvertKVStringsToMap(opts.labels.GetAll()),
  370. },
  371. TaskTemplate: swarm.TaskSpec{
  372. ContainerSpec: swarm.ContainerSpec{
  373. Image: opts.image,
  374. Args: opts.args,
  375. Env: opts.env.GetAll(),
  376. Dir: opts.workdir,
  377. User: opts.user,
  378. Mounts: opts.mounts.Value(),
  379. StopGracePeriod: opts.stopGrace.Value(),
  380. },
  381. Resources: opts.resources.ToResourceRequirements(),
  382. RestartPolicy: opts.restartPolicy.ToRestartPolicy(),
  383. Placement: &swarm.Placement{
  384. Constraints: opts.constraints,
  385. },
  386. LogDriver: opts.logDriver.toLogDriver(),
  387. },
  388. Mode: swarm.ServiceMode{},
  389. UpdateConfig: &swarm.UpdateConfig{
  390. Parallelism: opts.update.parallelism,
  391. Delay: opts.update.delay,
  392. },
  393. Networks: convertNetworks(opts.networks),
  394. EndpointSpec: opts.endpoint.ToEndpointSpec(),
  395. }
  396. switch opts.mode {
  397. case "global":
  398. if opts.replicas.Value() != nil {
  399. return service, fmt.Errorf("replicas can only be used with replicated mode")
  400. }
  401. service.Mode.Global = &swarm.GlobalService{}
  402. case "replicated":
  403. service.Mode.Replicated = &swarm.ReplicatedService{
  404. Replicas: opts.replicas.Value(),
  405. }
  406. default:
  407. return service, fmt.Errorf("Unknown mode: %s", opts.mode)
  408. }
  409. return service, nil
  410. }
  411. // addServiceFlags adds all flags that are common to both `create` and `update`.
  412. // Any flags that are not common are added separately in the individual command
  413. func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) {
  414. flags := cmd.Flags()
  415. flags.StringVar(&opts.name, flagName, "", "Service name")
  416. flags.StringVarP(&opts.workdir, "workdir", "w", "", "Working directory inside the container")
  417. flags.StringVarP(&opts.user, flagUser, "u", "", "Username or UID")
  418. flags.Var(&opts.resources.limitCPU, flagLimitCPU, "Limit CPUs")
  419. flags.Var(&opts.resources.limitMemBytes, flagLimitMemory, "Limit Memory")
  420. flags.Var(&opts.resources.resCPU, flagReserveCPU, "Reserve CPUs")
  421. flags.Var(&opts.resources.resMemBytes, flagReserveMemory, "Reserve Memory")
  422. flags.Var(&opts.stopGrace, flagStopGracePeriod, "Time to wait before force killing a container")
  423. flags.Var(&opts.replicas, flagReplicas, "Number of tasks")
  424. flags.StringVar(&opts.restartPolicy.condition, flagRestartCondition, "", "Restart when condition is met (none, on-failure, or any)")
  425. flags.Var(&opts.restartPolicy.delay, flagRestartDelay, "Delay between restart attempts")
  426. flags.Var(&opts.restartPolicy.maxAttempts, flagRestartMaxAttempts, "Maximum number of restarts before giving up")
  427. flags.Var(&opts.restartPolicy.window, flagRestartWindow, "Window used to evaluate the restart policy")
  428. flags.Uint64Var(&opts.update.parallelism, flagUpdateParallelism, 0, "Maximum number of tasks updated simultaneously")
  429. flags.DurationVar(&opts.update.delay, flagUpdateDelay, time.Duration(0), "Delay between updates")
  430. flags.StringVar(&opts.endpoint.mode, flagEndpointMode, "", "Endpoint mode (vip or dnsrr)")
  431. flags.BoolVar(&opts.registryAuth, flagRegistryAuth, false, "Send registry authentication details to Swarm agents")
  432. flags.StringVar(&opts.logDriver.name, flagLogDriver, "", "Logging driver for service")
  433. flags.Var(&opts.logDriver.opts, flagLogOpt, "Logging driver options")
  434. }
  435. const (
  436. flagConstraint = "constraint"
  437. flagConstraintRemove = "constraint-rm"
  438. flagConstraintAdd = "constraint-add"
  439. flagEndpointMode = "endpoint-mode"
  440. flagEnv = "env"
  441. flagEnvRemove = "env-rm"
  442. flagEnvAdd = "env-add"
  443. flagLabel = "label"
  444. flagLabelRemove = "label-rm"
  445. flagLabelAdd = "label-add"
  446. flagLimitCPU = "limit-cpu"
  447. flagLimitMemory = "limit-memory"
  448. flagMode = "mode"
  449. flagMount = "mount"
  450. flagMountRemove = "mount-rm"
  451. flagMountAdd = "mount-add"
  452. flagName = "name"
  453. flagNetwork = "network"
  454. flagNetworkRemove = "network-rm"
  455. flagNetworkAdd = "network-add"
  456. flagPublish = "publish"
  457. flagPublishRemove = "publish-rm"
  458. flagPublishAdd = "publish-add"
  459. flagReplicas = "replicas"
  460. flagReserveCPU = "reserve-cpu"
  461. flagReserveMemory = "reserve-memory"
  462. flagRestartCondition = "restart-condition"
  463. flagRestartDelay = "restart-delay"
  464. flagRestartMaxAttempts = "restart-max-attempts"
  465. flagRestartWindow = "restart-window"
  466. flagStopGracePeriod = "stop-grace-period"
  467. flagUpdateDelay = "update-delay"
  468. flagUpdateParallelism = "update-parallelism"
  469. flagUser = "user"
  470. flagRegistryAuth = "registry-auth"
  471. flagLogDriver = "log-driver"
  472. flagLogOpt = "log-opt"
  473. )