2016-09-08 13:11:39 -04:00
package stack
import (
"fmt"
2016-11-02 14:57:40 -04:00
"io/ioutil"
"os"
2016-10-31 15:43:47 -04:00
"sort"
2016-10-25 17:41:45 -04:00
"strings"
2016-09-08 13:11:39 -04:00
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/cli"
2016-09-08 14:54:01 -04:00
"github.com/docker/docker/cli/command"
2016-12-05 16:14:08 -05:00
"github.com/docker/docker/cli/compose/convert"
2016-12-20 16:26:49 -05:00
"github.com/docker/docker/cli/compose/loader"
composetypes "github.com/docker/docker/cli/compose/types"
2017-01-28 12:04:10 -05:00
apiclient "github.com/docker/docker/client"
2016-11-28 11:38:41 -05:00
dockerclient "github.com/docker/docker/client"
2017-01-13 11:26:29 -05:00
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/net/context"
2016-09-08 13:11:39 -04:00
)
const (
defaultNetworkDriver = "overlay"
)
type deployOptions struct {
2016-11-08 12:05:23 -05:00
bundlefile string
2016-11-02 14:57:40 -04:00
composefile string
2016-09-08 13:11:39 -04:00
namespace string
sendRegistryAuth bool
}
2016-09-08 14:54:01 -04:00
func newDeployCommand ( dockerCli * command . DockerCli ) * cobra . Command {
2016-09-08 13:11:39 -04:00
var opts deployOptions
cmd := & cobra . Command {
Use : "deploy [OPTIONS] STACK" ,
Aliases : [ ] string { "up" } ,
2016-11-02 14:57:40 -04:00
Short : "Deploy a new stack or update an existing stack" ,
2016-09-08 13:11:39 -04:00
Args : cli . ExactArgs ( 1 ) ,
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
2016-11-02 14:57:40 -04:00
opts . namespace = args [ 0 ]
2016-09-08 13:11:39 -04:00
return runDeploy ( dockerCli , opts )
} ,
}
flags := cmd . Flags ( )
2016-11-08 12:05:23 -05:00
addBundlefileFlag ( & opts . bundlefile , flags )
2016-11-02 14:57:40 -04:00
addComposefileFlag ( & opts . composefile , flags )
2016-09-08 13:11:39 -04:00
addRegistryAuthFlag ( & opts . sendRegistryAuth , flags )
return cmd
}
2016-09-08 14:54:01 -04:00
func runDeploy ( dockerCli * command . DockerCli , opts deployOptions ) error {
2016-11-21 15:03:43 -05:00
ctx := context . Background ( )
2016-11-08 15:20:16 -05:00
switch {
case opts . bundlefile == "" && opts . composefile == "" :
2016-11-08 12:05:23 -05:00
return fmt . Errorf ( "Please specify either a bundle file (with --bundle-file) or a Compose file (with --compose-file)." )
2016-11-08 15:20:16 -05:00
case opts . bundlefile != "" && opts . composefile != "" :
2016-11-08 12:05:23 -05:00
return fmt . Errorf ( "You cannot specify both a bundle file and a Compose file." )
2016-11-08 15:20:16 -05:00
case opts . bundlefile != "" :
2016-11-21 15:03:43 -05:00
return deployBundle ( ctx , dockerCli , opts )
2016-11-08 15:20:16 -05:00
default :
2016-11-21 15:03:43 -05:00
return deployCompose ( ctx , dockerCli , opts )
}
}
// checkDaemonIsSwarmManager does an Info API call to verify that the daemon is
// a swarm manager. This is necessary because we must create networks before we
// create services, but the API call for creating a network does not return a
// proper status code when it can't create a network in the "global" scope.
func checkDaemonIsSwarmManager ( ctx context . Context , dockerCli * command . DockerCli ) error {
info , err := dockerCli . Client ( ) . Info ( ctx )
if err != nil {
return err
}
if ! info . Swarm . ControlAvailable {
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." )
2016-11-08 12:05:23 -05:00
}
2016-11-21 15:03:43 -05:00
return nil
2016-11-08 12:05:23 -05:00
}
2016-11-21 15:03:43 -05:00
func deployCompose ( ctx context . Context , dockerCli * command . DockerCli , opts deployOptions ) error {
2016-11-02 14:57:40 -04:00
configDetails , err := getConfigDetails ( opts )
2016-09-08 13:11:39 -04:00
if err != nil {
return err
}
2016-11-02 14:57:40 -04:00
config , err := loader . Load ( configDetails )
2016-09-08 13:11:39 -04:00
if err != nil {
2016-10-31 15:43:47 -04:00
if fpe , ok := err . ( * loader . ForbiddenPropertiesError ) ; ok {
return fmt . Errorf ( "Compose file contains unsupported options:\n\n%s\n" ,
propertyWarnings ( fpe . Properties ) )
}
2016-09-08 13:11:39 -04:00
return err
}
2016-10-31 15:43:47 -04:00
unsupportedProperties := loader . GetUnsupportedProperties ( configDetails )
if len ( unsupportedProperties ) > 0 {
2016-11-02 12:19:37 -04:00
fmt . Fprintf ( dockerCli . Err ( ) , "Ignoring unsupported options: %s\n\n" ,
2016-10-31 15:43:47 -04:00
strings . Join ( unsupportedProperties , ", " ) )
}
deprecatedProperties := loader . GetDeprecatedProperties ( configDetails )
if len ( deprecatedProperties ) > 0 {
2016-11-02 12:19:37 -04:00
fmt . Fprintf ( dockerCli . Err ( ) , "Ignoring deprecated options:\n\n%s\n\n" ,
2016-10-31 15:43:47 -04:00
propertyWarnings ( deprecatedProperties ) )
}
2016-11-21 15:03:43 -05:00
if err := checkDaemonIsSwarmManager ( ctx , dockerCli ) ; err != nil {
return err
}
2016-12-05 16:14:08 -05:00
namespace := convert . NewNamespace ( opts . namespace )
2016-11-04 16:55:24 -04:00
2017-01-10 03:57:36 -05:00
serviceNetworks := getServicesDeclaredNetworks ( config . Services )
networks , externalNetworks := convert . Networks ( namespace , config . Networks , serviceNetworks )
2016-11-28 11:38:41 -05:00
if err := validateExternalNetworks ( ctx , dockerCli , externalNetworks ) ; err != nil {
return err
}
2016-11-08 12:05:23 -05:00
if err := createNetworks ( ctx , dockerCli , namespace , networks ) ; err != nil {
return err
2016-11-04 16:55:24 -04:00
}
2017-01-10 17:40:53 -05:00
secrets , err := convert . Secrets ( namespace , config . Secrets )
if err != nil {
return err
}
if err := createSecrets ( ctx , dockerCli , namespace , secrets ) ; err != nil {
return err
}
services , err := convert . Services ( namespace , config , dockerCli . Client ( ) )
2016-11-08 12:05:23 -05:00
if err != nil {
2016-09-08 13:11:39 -04:00
return err
}
2016-11-08 12:05:23 -05:00
return deployServices ( ctx , dockerCli , services , namespace , opts . sendRegistryAuth )
2016-09-08 13:11:39 -04:00
}
2017-01-10 03:57:36 -05:00
func getServicesDeclaredNetworks ( serviceConfigs [ ] composetypes . ServiceConfig ) map [ string ] struct { } {
serviceNetworks := map [ string ] struct { } { }
for _ , serviceConfig := range serviceConfigs {
if len ( serviceConfig . Networks ) == 0 {
serviceNetworks [ "default" ] = struct { } { }
continue
}
for network := range serviceConfig . Networks {
serviceNetworks [ network ] = struct { } { }
}
}
return serviceNetworks
}
2016-10-31 15:43:47 -04:00
func propertyWarnings ( properties map [ string ] string ) string {
var msgs [ ] string
for name , description := range properties {
msgs = append ( msgs , fmt . Sprintf ( "%s: %s" , name , description ) )
}
sort . Strings ( msgs )
return strings . Join ( msgs , "\n\n" )
}
2016-11-02 14:57:40 -04:00
func getConfigDetails ( opts deployOptions ) ( composetypes . ConfigDetails , error ) {
var details composetypes . ConfigDetails
var err error
details . WorkingDir , err = os . Getwd ( )
if err != nil {
return details , err
}
configFile , err := getConfigFile ( opts . composefile )
if err != nil {
return details , err
2016-09-08 13:11:39 -04:00
}
2016-11-02 14:57:40 -04:00
// TODO: support multiple files
details . ConfigFiles = [ ] composetypes . ConfigFile { * configFile }
return details , nil
}
2016-09-08 13:11:39 -04:00
2016-11-02 14:57:40 -04:00
func getConfigFile ( filename string ) ( * composetypes . ConfigFile , error ) {
bytes , err := ioutil . ReadFile ( filename )
if err != nil {
return nil , err
2016-09-08 13:11:39 -04:00
}
2016-10-25 17:41:45 -04:00
config , err := loader . ParseYAML ( bytes )
if err != nil {
return nil , err
}
return & composetypes . ConfigFile {
Filename : filename ,
Config : config ,
} , nil
2016-09-08 13:11:39 -04:00
}
2016-11-28 11:38:41 -05:00
func validateExternalNetworks (
ctx context . Context ,
dockerCli * command . DockerCli ,
externalNetworks [ ] string ) error {
client := dockerCli . Client ( )
for _ , networkName := range externalNetworks {
network , err := client . NetworkInspect ( ctx , networkName )
if err != nil {
if dockerclient . IsErrNetworkNotFound ( err ) {
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 )
}
return err
}
if network . Scope != "swarm" {
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" )
}
}
return nil
2016-11-08 12:05:23 -05:00
}
2017-01-10 17:40:53 -05:00
func createSecrets (
ctx context . Context ,
dockerCli * command . DockerCli ,
namespace convert . Namespace ,
secrets [ ] swarm . SecretSpec ,
) error {
client := dockerCli . Client ( )
2017-01-13 11:26:29 -05:00
for _ , secretSpec := range secrets {
2017-01-28 12:04:10 -05:00
secret , _ , err := client . SecretInspectWithRaw ( ctx , secretSpec . Name )
if err == nil {
// secret already exists, then we update that
if err := client . SecretUpdate ( ctx , secret . ID , secret . Meta . Version , secretSpec ) ; err != nil {
return err
}
} else if apiclient . IsErrSecretNotFound ( err ) {
// secret does not exist, then we create a new one.
if _ , err := client . SecretCreate ( ctx , secretSpec ) ; err != nil {
return err
}
} else {
2017-01-10 17:40:53 -05:00
return err
}
}
return nil
}
2016-11-08 12:05:23 -05:00
func createNetworks (
ctx context . Context ,
dockerCli * command . DockerCli ,
2016-12-05 16:14:08 -05:00
namespace convert . Namespace ,
2016-11-08 12:05:23 -05:00
networks map [ string ] types . NetworkCreate ,
) error {
client := dockerCli . Client ( )
2016-11-30 16:34:29 -05:00
existingNetworks , err := getStackNetworks ( ctx , client , namespace . Name ( ) )
2016-11-08 12:05:23 -05:00
if err != nil {
return err
}
existingNetworkMap := make ( map [ string ] types . NetworkResource )
for _ , network := range existingNetworks {
existingNetworkMap [ network . Name ] = network
}
for internalName , createOpts := range networks {
2016-11-30 16:34:29 -05:00
name := namespace . Scope ( internalName )
2016-11-08 12:05:23 -05:00
if _ , exists := existingNetworkMap [ name ] ; exists {
continue
}
2016-11-02 14:57:40 -04:00
if createOpts . Driver == "" {
createOpts . Driver = defaultNetworkDriver
}
2016-09-08 13:11:39 -04:00
fmt . Fprintf ( dockerCli . Out ( ) , "Creating network %s\n" , name )
if _ , err := client . NetworkCreate ( ctx , name , createOpts ) ; err != nil {
return err
}
}
2016-11-08 12:05:23 -05:00
2016-09-08 13:11:39 -04:00
return nil
}
func deployServices (
ctx context . Context ,
2016-09-08 14:54:01 -04:00
dockerCli * command . DockerCli ,
2016-11-08 12:05:23 -05:00
services map [ string ] swarm . ServiceSpec ,
2016-12-05 16:14:08 -05:00
namespace convert . Namespace ,
2016-09-08 13:11:39 -04:00
sendAuth bool ,
) error {
apiClient := dockerCli . Client ( )
out := dockerCli . Out ( )
2016-11-30 17:38:40 -05:00
existingServices , err := getServices ( ctx , apiClient , namespace . Name ( ) )
2016-09-08 13:11:39 -04:00
if err != nil {
return err
}
existingServiceMap := make ( map [ string ] swarm . Service )
for _ , service := range existingServices {
existingServiceMap [ service . Spec . Name ] = service
}
2016-11-08 12:05:23 -05:00
for internalName , serviceSpec := range services {
2016-11-30 17:38:40 -05:00
name := namespace . Scope ( internalName )
2016-09-08 13:11:39 -04:00
encodedAuth := ""
if sendAuth {
// Retrieve encoded auth token from the image reference
image := serviceSpec . TaskTemplate . ContainerSpec . Image
2016-09-09 15:38:00 -04:00
encodedAuth , err = command . RetrieveAuthTokenFromImage ( ctx , dockerCli , image )
2016-09-08 13:11:39 -04:00
if err != nil {
return err
}
}
if service , exists := existingServiceMap [ name ] ; exists {
fmt . Fprintf ( out , "Updating service %s (id: %s)\n" , name , service . ID )
updateOpts := types . ServiceUpdateOptions { }
if sendAuth {
updateOpts . EncodedRegistryAuth = encodedAuth
}
2016-11-14 21:08:24 -05:00
response , err := apiClient . ServiceUpdate (
2016-09-08 13:11:39 -04:00
ctx ,
service . ID ,
service . Version ,
serviceSpec ,
updateOpts ,
2016-11-14 21:08:24 -05:00
)
if err != nil {
2016-09-08 13:11:39 -04:00
return err
}
2016-11-14 21:08:24 -05:00
for _ , warning := range response . Warnings {
fmt . Fprintln ( dockerCli . Err ( ) , warning )
}
2016-09-08 13:11:39 -04:00
} else {
fmt . Fprintf ( out , "Creating service %s\n" , name )
createOpts := types . ServiceCreateOptions { }
if sendAuth {
createOpts . EncodedRegistryAuth = encodedAuth
}
if _ , err := apiClient . ServiceCreate ( ctx , serviceSpec , createOpts ) ; err != nil {
return err
}
}
}
return nil
}