deploy.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. package stack
  2. import (
  3. "fmt"
  4. "io/ioutil"
  5. "os"
  6. "time"
  7. "github.com/spf13/cobra"
  8. "golang.org/x/net/context"
  9. "github.com/aanand/compose-file/loader"
  10. composetypes "github.com/aanand/compose-file/types"
  11. "github.com/docker/docker/api/types"
  12. networktypes "github.com/docker/docker/api/types/network"
  13. "github.com/docker/docker/api/types/swarm"
  14. "github.com/docker/docker/cli"
  15. "github.com/docker/docker/cli/command"
  16. servicecmd "github.com/docker/docker/cli/command/service"
  17. "github.com/docker/go-connections/nat"
  18. )
  19. const (
  20. defaultNetworkDriver = "overlay"
  21. )
  22. type deployOptions struct {
  23. composefile string
  24. namespace string
  25. sendRegistryAuth bool
  26. }
  27. func newDeployCommand(dockerCli *command.DockerCli) *cobra.Command {
  28. var opts deployOptions
  29. cmd := &cobra.Command{
  30. Use: "deploy [OPTIONS] STACK",
  31. Aliases: []string{"up"},
  32. Short: "Deploy a new stack or update an existing stack",
  33. Args: cli.ExactArgs(1),
  34. RunE: func(cmd *cobra.Command, args []string) error {
  35. opts.namespace = args[0]
  36. return runDeploy(dockerCli, opts)
  37. },
  38. Tags: map[string]string{"experimental": "", "version": "1.25"},
  39. }
  40. flags := cmd.Flags()
  41. addComposefileFlag(&opts.composefile, flags)
  42. addRegistryAuthFlag(&opts.sendRegistryAuth, flags)
  43. return cmd
  44. }
  45. func runDeploy(dockerCli *command.DockerCli, opts deployOptions) error {
  46. configDetails, err := getConfigDetails(opts)
  47. if err != nil {
  48. return err
  49. }
  50. config, err := loader.Load(configDetails)
  51. if err != nil {
  52. return err
  53. }
  54. ctx := context.Background()
  55. if err := createNetworks(ctx, dockerCli, config.Networks, opts.namespace); err != nil {
  56. return err
  57. }
  58. return deployServices(ctx, dockerCli, config, opts.namespace, opts.sendRegistryAuth)
  59. }
  60. func getConfigDetails(opts deployOptions) (composetypes.ConfigDetails, error) {
  61. var details composetypes.ConfigDetails
  62. var err error
  63. details.WorkingDir, err = os.Getwd()
  64. if err != nil {
  65. return details, err
  66. }
  67. configFile, err := getConfigFile(opts.composefile)
  68. if err != nil {
  69. return details, err
  70. }
  71. // TODO: support multiple files
  72. details.ConfigFiles = []composetypes.ConfigFile{*configFile}
  73. return details, nil
  74. }
  75. func getConfigFile(filename string) (*composetypes.ConfigFile, error) {
  76. bytes, err := ioutil.ReadFile(filename)
  77. if err != nil {
  78. return nil, err
  79. }
  80. return loader.ParseYAML(bytes, filename)
  81. }
  82. func createNetworks(
  83. ctx context.Context,
  84. dockerCli *command.DockerCli,
  85. networks map[string]composetypes.NetworkConfig,
  86. namespace string,
  87. ) error {
  88. client := dockerCli.Client()
  89. existingNetworks, err := getNetworks(ctx, client, namespace)
  90. if err != nil {
  91. return err
  92. }
  93. existingNetworkMap := make(map[string]types.NetworkResource)
  94. for _, network := range existingNetworks {
  95. existingNetworkMap[network.Name] = network
  96. }
  97. for internalName, network := range networks {
  98. if network.ExternalName != "" {
  99. continue
  100. }
  101. name := fmt.Sprintf("%s_%s", namespace, internalName)
  102. if _, exists := existingNetworkMap[name]; exists {
  103. continue
  104. }
  105. createOpts := types.NetworkCreate{
  106. // TODO: support network labels from compose file
  107. Labels: getStackLabels(namespace, nil),
  108. Driver: network.Driver,
  109. Options: network.DriverOpts,
  110. }
  111. if network.Ipam.Driver != "" {
  112. createOpts.IPAM = &networktypes.IPAM{
  113. Driver: network.Ipam.Driver,
  114. }
  115. }
  116. // TODO: IPAMConfig.Config
  117. if createOpts.Driver == "" {
  118. createOpts.Driver = defaultNetworkDriver
  119. }
  120. fmt.Fprintf(dockerCli.Out(), "Creating network %s\n", name)
  121. if _, err := client.NetworkCreate(ctx, name, createOpts); err != nil {
  122. return err
  123. }
  124. }
  125. return nil
  126. }
  127. func convertNetworks(
  128. networks map[string]*composetypes.ServiceNetworkConfig,
  129. namespace string,
  130. name string,
  131. ) []swarm.NetworkAttachmentConfig {
  132. nets := []swarm.NetworkAttachmentConfig{}
  133. for networkName, network := range networks {
  134. nets = append(nets, swarm.NetworkAttachmentConfig{
  135. // TODO: only do this name mangling in one function
  136. Target: namespace + "_" + networkName,
  137. Aliases: append(network.Aliases, name),
  138. })
  139. }
  140. return nets
  141. }
  142. func deployServices(
  143. ctx context.Context,
  144. dockerCli *command.DockerCli,
  145. config *composetypes.Config,
  146. namespace string,
  147. sendAuth bool,
  148. ) error {
  149. apiClient := dockerCli.Client()
  150. out := dockerCli.Out()
  151. services := config.Services
  152. volumes := config.Volumes
  153. existingServices, err := getServices(ctx, apiClient, namespace)
  154. if err != nil {
  155. return err
  156. }
  157. existingServiceMap := make(map[string]swarm.Service)
  158. for _, service := range existingServices {
  159. existingServiceMap[service.Spec.Name] = service
  160. }
  161. for _, service := range services {
  162. name := fmt.Sprintf("%s_%s", namespace, service.Name)
  163. serviceSpec, err := convertService(namespace, service, volumes)
  164. if err != nil {
  165. return err
  166. }
  167. encodedAuth := ""
  168. if sendAuth {
  169. // Retrieve encoded auth token from the image reference
  170. image := serviceSpec.TaskTemplate.ContainerSpec.Image
  171. encodedAuth, err = command.RetrieveAuthTokenFromImage(ctx, dockerCli, image)
  172. if err != nil {
  173. return err
  174. }
  175. }
  176. if service, exists := existingServiceMap[name]; exists {
  177. fmt.Fprintf(out, "Updating service %s (id: %s)\n", name, service.ID)
  178. updateOpts := types.ServiceUpdateOptions{}
  179. if sendAuth {
  180. updateOpts.EncodedRegistryAuth = encodedAuth
  181. }
  182. if err := apiClient.ServiceUpdate(
  183. ctx,
  184. service.ID,
  185. service.Version,
  186. serviceSpec,
  187. updateOpts,
  188. ); err != nil {
  189. return err
  190. }
  191. } else {
  192. fmt.Fprintf(out, "Creating service %s\n", name)
  193. createOpts := types.ServiceCreateOptions{}
  194. if sendAuth {
  195. createOpts.EncodedRegistryAuth = encodedAuth
  196. }
  197. if _, err := apiClient.ServiceCreate(ctx, serviceSpec, createOpts); err != nil {
  198. return err
  199. }
  200. }
  201. }
  202. return nil
  203. }
  204. func convertService(
  205. namespace string,
  206. service composetypes.ServiceConfig,
  207. volumes map[string]composetypes.VolumeConfig,
  208. ) (swarm.ServiceSpec, error) {
  209. // TODO: remove this duplication
  210. name := fmt.Sprintf("%s_%s", namespace, service.Name)
  211. endpoint, err := convertEndpointSpec(service.Ports)
  212. if err != nil {
  213. return swarm.ServiceSpec{}, err
  214. }
  215. mode, err := convertDeployMode(service.Deploy.Mode, service.Deploy.Replicas)
  216. if err != nil {
  217. return swarm.ServiceSpec{}, err
  218. }
  219. serviceSpec := swarm.ServiceSpec{
  220. Annotations: swarm.Annotations{
  221. Name: name,
  222. Labels: getStackLabels(namespace, service.Labels),
  223. },
  224. TaskTemplate: swarm.TaskSpec{
  225. ContainerSpec: swarm.ContainerSpec{
  226. Image: service.Image,
  227. Command: service.Entrypoint,
  228. Args: service.Command,
  229. Env: convertEnvironment(service.Environment),
  230. Labels: getStackLabels(namespace, service.Deploy.Labels),
  231. Dir: service.WorkingDir,
  232. User: service.User,
  233. },
  234. Placement: &swarm.Placement{
  235. Constraints: service.Deploy.Placement.Constraints,
  236. },
  237. },
  238. EndpointSpec: endpoint,
  239. Mode: mode,
  240. Networks: convertNetworks(service.Networks, namespace, service.Name),
  241. }
  242. if service.StopGracePeriod != nil {
  243. stopGrace, err := time.ParseDuration(*service.StopGracePeriod)
  244. if err != nil {
  245. return swarm.ServiceSpec{}, err
  246. }
  247. serviceSpec.TaskTemplate.ContainerSpec.StopGracePeriod = &stopGrace
  248. }
  249. // TODO: convert mounts
  250. return serviceSpec, nil
  251. }
  252. func convertEndpointSpec(source []string) (*swarm.EndpointSpec, error) {
  253. portConfigs := []swarm.PortConfig{}
  254. ports, portBindings, err := nat.ParsePortSpecs(source)
  255. if err != nil {
  256. return nil, err
  257. }
  258. for port := range ports {
  259. portConfigs = append(
  260. portConfigs,
  261. servicecmd.ConvertPortToPortConfig(port, portBindings)...)
  262. }
  263. return &swarm.EndpointSpec{Ports: portConfigs}, nil
  264. }
  265. func convertEnvironment(source map[string]string) []string {
  266. var output []string
  267. for name, value := range source {
  268. output = append(output, fmt.Sprintf("%s=%s", name, value))
  269. }
  270. return output
  271. }
  272. func convertDeployMode(mode string, replicas uint64) (swarm.ServiceMode, error) {
  273. serviceMode := swarm.ServiceMode{}
  274. switch mode {
  275. case "global":
  276. if replicas != 0 {
  277. return serviceMode, fmt.Errorf("replicas can only be used with replicated mode")
  278. }
  279. serviceMode.Global = &swarm.GlobalService{}
  280. case "replicated":
  281. serviceMode.Replicated = &swarm.ReplicatedService{Replicas: &replicas}
  282. default:
  283. return serviceMode, fmt.Errorf("Unknown mode: %s", mode)
  284. }
  285. return serviceMode, nil
  286. }