github.com/flavio/docker@v0.1.3-0.20170117145210-f63d1a6eec47/cli/command/stack/deploy.go (about) 1 package stack 2 3 import ( 4 "errors" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "sort" 9 "strings" 10 11 "github.com/spf13/cobra" 12 "golang.org/x/net/context" 13 14 "github.com/docker/docker/api/types" 15 "github.com/docker/docker/api/types/swarm" 16 "github.com/docker/docker/cli" 17 "github.com/docker/docker/cli/command" 18 "github.com/docker/docker/cli/compose/convert" 19 "github.com/docker/docker/cli/compose/loader" 20 composetypes "github.com/docker/docker/cli/compose/types" 21 dockerclient "github.com/docker/docker/client" 22 ) 23 24 const ( 25 defaultNetworkDriver = "overlay" 26 ) 27 28 type deployOptions struct { 29 bundlefile string 30 composefile string 31 namespace string 32 sendRegistryAuth bool 33 } 34 35 func newDeployCommand(dockerCli *command.DockerCli) *cobra.Command { 36 var opts deployOptions 37 38 cmd := &cobra.Command{ 39 Use: "deploy [OPTIONS] STACK", 40 Aliases: []string{"up"}, 41 Short: "Deploy a new stack or update an existing stack", 42 Args: cli.ExactArgs(1), 43 RunE: func(cmd *cobra.Command, args []string) error { 44 opts.namespace = args[0] 45 return runDeploy(dockerCli, opts) 46 }, 47 } 48 49 flags := cmd.Flags() 50 addBundlefileFlag(&opts.bundlefile, flags) 51 addComposefileFlag(&opts.composefile, flags) 52 addRegistryAuthFlag(&opts.sendRegistryAuth, flags) 53 return cmd 54 } 55 56 func runDeploy(dockerCli *command.DockerCli, opts deployOptions) error { 57 ctx := context.Background() 58 59 switch { 60 case opts.bundlefile == "" && opts.composefile == "": 61 return fmt.Errorf("Please specify either a bundle file (with --bundle-file) or a Compose file (with --compose-file).") 62 case opts.bundlefile != "" && opts.composefile != "": 63 return fmt.Errorf("You cannot specify both a bundle file and a Compose file.") 64 case opts.bundlefile != "": 65 return deployBundle(ctx, dockerCli, opts) 66 default: 67 return deployCompose(ctx, dockerCli, opts) 68 } 69 } 70 71 // checkDaemonIsSwarmManager does an Info API call to verify that the daemon is 72 // a swarm manager. This is necessary because we must create networks before we 73 // create services, but the API call for creating a network does not return a 74 // proper status code when it can't create a network in the "global" scope. 75 func checkDaemonIsSwarmManager(ctx context.Context, dockerCli *command.DockerCli) error { 76 info, err := dockerCli.Client().Info(ctx) 77 if err != nil { 78 return err 79 } 80 if !info.Swarm.ControlAvailable { 81 return errors.New("This node is not a swarm manager. Use \"docker swarm init\" or \"docker swarm join\" to connect this node to swarm and try again.") 82 } 83 return nil 84 } 85 86 func deployCompose(ctx context.Context, dockerCli *command.DockerCli, opts deployOptions) error { 87 configDetails, err := getConfigDetails(opts) 88 if err != nil { 89 return err 90 } 91 92 config, err := loader.Load(configDetails) 93 if err != nil { 94 if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok { 95 return fmt.Errorf("Compose file contains unsupported options:\n\n%s\n", 96 propertyWarnings(fpe.Properties)) 97 } 98 99 return err 100 } 101 102 unsupportedProperties := loader.GetUnsupportedProperties(configDetails) 103 if len(unsupportedProperties) > 0 { 104 fmt.Fprintf(dockerCli.Err(), "Ignoring unsupported options: %s\n\n", 105 strings.Join(unsupportedProperties, ", ")) 106 } 107 108 deprecatedProperties := loader.GetDeprecatedProperties(configDetails) 109 if len(deprecatedProperties) > 0 { 110 fmt.Fprintf(dockerCli.Err(), "Ignoring deprecated options:\n\n%s\n\n", 111 propertyWarnings(deprecatedProperties)) 112 } 113 114 if err := checkDaemonIsSwarmManager(ctx, dockerCli); err != nil { 115 return err 116 } 117 118 namespace := convert.NewNamespace(opts.namespace) 119 120 serviceNetworks := getServicesDeclaredNetworks(config.Services) 121 122 networks, externalNetworks := convert.Networks(namespace, config.Networks, serviceNetworks) 123 if err := validateExternalNetworks(ctx, dockerCli, externalNetworks); err != nil { 124 return err 125 } 126 if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil { 127 return err 128 } 129 services, err := convert.Services(namespace, config) 130 if err != nil { 131 return err 132 } 133 return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth) 134 } 135 136 func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} { 137 serviceNetworks := map[string]struct{}{} 138 for _, serviceConfig := range serviceConfigs { 139 if len(serviceConfig.Networks) == 0 { 140 serviceNetworks["default"] = struct{}{} 141 continue 142 } 143 for network := range serviceConfig.Networks { 144 serviceNetworks[network] = struct{}{} 145 } 146 } 147 return serviceNetworks 148 } 149 150 func propertyWarnings(properties map[string]string) string { 151 var msgs []string 152 for name, description := range properties { 153 msgs = append(msgs, fmt.Sprintf("%s: %s", name, description)) 154 } 155 sort.Strings(msgs) 156 return strings.Join(msgs, "\n\n") 157 } 158 159 func getConfigDetails(opts deployOptions) (composetypes.ConfigDetails, error) { 160 var details composetypes.ConfigDetails 161 var err error 162 163 details.WorkingDir, err = os.Getwd() 164 if err != nil { 165 return details, err 166 } 167 168 configFile, err := getConfigFile(opts.composefile) 169 if err != nil { 170 return details, err 171 } 172 // TODO: support multiple files 173 details.ConfigFiles = []composetypes.ConfigFile{*configFile} 174 return details, nil 175 } 176 177 func getConfigFile(filename string) (*composetypes.ConfigFile, error) { 178 bytes, err := ioutil.ReadFile(filename) 179 if err != nil { 180 return nil, err 181 } 182 config, err := loader.ParseYAML(bytes) 183 if err != nil { 184 return nil, err 185 } 186 return &composetypes.ConfigFile{ 187 Filename: filename, 188 Config: config, 189 }, nil 190 } 191 192 func validateExternalNetworks( 193 ctx context.Context, 194 dockerCli *command.DockerCli, 195 externalNetworks []string) error { 196 client := dockerCli.Client() 197 198 for _, networkName := range externalNetworks { 199 network, err := client.NetworkInspect(ctx, networkName) 200 if err != nil { 201 if dockerclient.IsErrNetworkNotFound(err) { 202 return fmt.Errorf("network %q is declared as external, but could not be found. You need to create the network before the stack is deployed (with overlay driver)", networkName) 203 } 204 return err 205 } 206 if network.Scope != "swarm" { 207 return fmt.Errorf("network %q is declared as external, but it is not in the right scope: %q instead of %q", networkName, network.Scope, "swarm") 208 } 209 } 210 211 return nil 212 } 213 214 func createNetworks( 215 ctx context.Context, 216 dockerCli *command.DockerCli, 217 namespace convert.Namespace, 218 networks map[string]types.NetworkCreate, 219 ) error { 220 client := dockerCli.Client() 221 222 existingNetworks, err := getStackNetworks(ctx, client, namespace.Name()) 223 if err != nil { 224 return err 225 } 226 227 existingNetworkMap := make(map[string]types.NetworkResource) 228 for _, network := range existingNetworks { 229 existingNetworkMap[network.Name] = network 230 } 231 232 for internalName, createOpts := range networks { 233 name := namespace.Scope(internalName) 234 if _, exists := existingNetworkMap[name]; exists { 235 continue 236 } 237 238 if createOpts.Driver == "" { 239 createOpts.Driver = defaultNetworkDriver 240 } 241 242 fmt.Fprintf(dockerCli.Out(), "Creating network %s\n", name) 243 if _, err := client.NetworkCreate(ctx, name, createOpts); err != nil { 244 return err 245 } 246 } 247 248 return nil 249 } 250 251 func deployServices( 252 ctx context.Context, 253 dockerCli *command.DockerCli, 254 services map[string]swarm.ServiceSpec, 255 namespace convert.Namespace, 256 sendAuth bool, 257 ) error { 258 apiClient := dockerCli.Client() 259 out := dockerCli.Out() 260 261 existingServices, err := getServices(ctx, apiClient, namespace.Name()) 262 if err != nil { 263 return err 264 } 265 266 existingServiceMap := make(map[string]swarm.Service) 267 for _, service := range existingServices { 268 existingServiceMap[service.Spec.Name] = service 269 } 270 271 for internalName, serviceSpec := range services { 272 name := namespace.Scope(internalName) 273 274 encodedAuth := "" 275 if sendAuth { 276 // Retrieve encoded auth token from the image reference 277 image := serviceSpec.TaskTemplate.ContainerSpec.Image 278 encodedAuth, err = command.RetrieveAuthTokenFromImage(ctx, dockerCli, image) 279 if err != nil { 280 return err 281 } 282 } 283 284 if service, exists := existingServiceMap[name]; exists { 285 fmt.Fprintf(out, "Updating service %s (id: %s)\n", name, service.ID) 286 287 updateOpts := types.ServiceUpdateOptions{} 288 if sendAuth { 289 updateOpts.EncodedRegistryAuth = encodedAuth 290 } 291 response, err := apiClient.ServiceUpdate( 292 ctx, 293 service.ID, 294 service.Version, 295 serviceSpec, 296 updateOpts, 297 ) 298 if err != nil { 299 return err 300 } 301 302 for _, warning := range response.Warnings { 303 fmt.Fprintln(dockerCli.Err(), warning) 304 } 305 } else { 306 fmt.Fprintf(out, "Creating service %s\n", name) 307 308 createOpts := types.ServiceCreateOptions{} 309 if sendAuth { 310 createOpts.EncodedRegistryAuth = encodedAuth 311 } 312 if _, err := apiClient.ServiceCreate(ctx, serviceSpec, createOpts); err != nil { 313 return err 314 } 315 } 316 } 317 318 return nil 319 }