2017-09-26 12:50:09 +00:00
package docker
import (
"context"
"fmt"
"sort"
"github.com/cuigh/auxo/errors"
"github.com/cuigh/swirl/biz/docker/compose"
"github.com/cuigh/swirl/model"
"github.com/docker/docker/api/types"
2017-09-27 06:30:00 +00:00
"github.com/docker/docker/api/types/container"
2017-09-26 12:50:09 +00:00
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
)
const stackLabel = "com.docker.stack.namespace"
// StackList return all stacks.
func StackList ( ) ( stacks [ ] * model . StackListInfo , err error ) {
err = mgr . Do ( func ( ctx context . Context , cli * client . Client ) ( err error ) {
var services [ ] swarm . Service
opts := types . ServiceListOptions {
Filters : filters . NewArgs ( ) ,
}
opts . Filters . Add ( "label" , stackLabel )
services , err = cli . ServiceList ( ctx , opts )
if err != nil {
return
}
m := make ( map [ string ] * model . StackListInfo )
for _ , service := range services {
labels := service . Spec . Labels
name , ok := labels [ stackLabel ]
if ! ok {
err = fmt . Errorf ( "cannot get label %s for service %s(%s)" , stackLabel , service . Spec . Name , service . ID )
return
}
if stack , ok := m [ name ] ; ok {
stack . Services = append ( stack . Services , service . Spec . Name )
} else {
m [ name ] = & model . StackListInfo {
Name : name ,
Services : [ ] string { service . Spec . Name } ,
}
}
}
for _ , stack := range m {
stacks = append ( stacks , stack )
}
sort . Slice ( stacks , func ( i , j int ) bool {
return stacks [ i ] . Name < stacks [ j ] . Name
} )
return
} )
return
}
// StackCount return number of stacks.
func StackCount ( ) ( count int , err error ) {
err = mgr . Do ( func ( ctx context . Context , cli * client . Client ) ( err error ) {
var services [ ] swarm . Service
opts := types . ServiceListOptions {
Filters : filters . NewArgs ( ) ,
}
opts . Filters . Add ( "label" , stackLabel )
services , err = cli . ServiceList ( ctx , opts )
if err != nil {
return
}
m := make ( map [ string ] struct { } )
for _ , service := range services {
labels := service . Spec . Labels
if name , ok := labels [ stackLabel ] ; ok {
m [ name ] = struct { } { }
} else {
mgr . Logger ( ) . Warnf ( "cannot get label %s for service %s(%s)" , stackLabel , service . Spec . Name , service . ID )
}
}
count = len ( m )
return
} )
return
}
// StackRemove remove a stack.
func StackRemove ( name string ) error {
return mgr . Do ( func ( ctx context . Context , cli * client . Client ) ( err error ) {
var (
services [ ] swarm . Service
networks [ ] types . NetworkResource
secrets [ ] swarm . Secret
configs [ ] swarm . Config
errs [ ] error
)
args := filters . NewArgs ( )
args . Add ( "label" , stackLabel + "=" + name )
services , err = cli . ServiceList ( ctx , types . ServiceListOptions { Filters : args } )
if err != nil {
return
}
networks , err = cli . NetworkList ( ctx , types . NetworkListOptions { Filters : args } )
if err != nil {
return
}
// API version >= 1.25
secrets , err = cli . SecretList ( ctx , types . SecretListOptions { Filters : args } )
if err != nil {
return
}
// API version >= 1.30
configs , err = cli . ConfigList ( ctx , types . ConfigListOptions { Filters : args } )
if err != nil {
return
}
if len ( services ) + len ( networks ) + len ( secrets ) + len ( configs ) == 0 {
return fmt . Errorf ( "nothing found in stack: %s" , name )
}
// Remove services
for _ , service := range services {
if err = cli . ServiceRemove ( ctx , service . ID ) ; err != nil {
e := errors . Format ( "Failed to remove service %s: %s" , service . Spec . Name , err )
errs = append ( errs , e )
mgr . Logger ( ) . Warn ( e )
}
}
// Remove secrets
for _ , secret := range secrets {
if err = cli . SecretRemove ( ctx , secret . ID ) ; err != nil {
e := errors . Format ( "Failed to remove secret %s: %s" , secret . Spec . Name , err )
errs = append ( errs , e )
mgr . Logger ( ) . Warn ( e )
}
}
// Remove configs
for _ , config := range configs {
if err = cli . ConfigRemove ( ctx , config . ID ) ; err != nil {
e := errors . Format ( "Failed to remove config %s: %s" , config . Spec . Name , err )
errs = append ( errs , e )
mgr . Logger ( ) . Warn ( e )
}
}
// Remove networks
for _ , network := range networks {
if err = cli . NetworkRemove ( ctx , network . ID ) ; err != nil {
e := errors . Format ( "Failed to remove network %s: %s" , network . Name , err )
errs = append ( errs , e )
mgr . Logger ( ) . Warn ( e )
}
}
if len ( errs ) > 0 {
return errors . Multi ( errs ... )
}
return nil
} )
}
// StackDeploy deploy a stack.
func StackDeploy ( name , content string , authes map [ string ] string ) error {
ctx , cli , err := mgr . Client ( )
if err != nil {
return err
}
cfg , err := compose . Parse ( name , content )
if err != nil {
return err
}
namespace := compose . NewNamespace ( name )
serviceNetworks := compose . GetServicesDeclaredNetworks ( cfg . Services )
networks , externalNetworks := compose . Networks ( namespace , cfg . Networks , serviceNetworks )
if err = validateExternalNetworks ( ctx , cli , externalNetworks ) ; err != nil {
return err
}
if err = createNetworks ( ctx , cli , namespace , networks ) ; err != nil {
return err
}
secrets , err := compose . Secrets ( namespace , cfg . Secrets )
if err != nil {
return err
}
if err = createSecrets ( ctx , cli , secrets ) ; err != nil {
return err
}
configs , err := compose . Configs ( namespace , cfg . Configs )
if err != nil {
return err
}
if err = createConfigs ( ctx , cli , configs ) ; err != nil {
return err
}
services , err := compose . Services ( namespace , cfg , cli )
if err != nil {
return err
}
return deployServices ( ctx , cli , services , namespace , authes )
}
func validateExternalNetworks ( ctx context . Context , cli * client . Client , externalNetworks [ ] string ) error {
for _ , networkName := range externalNetworks {
2017-09-27 06:30:00 +00:00
if ! container . NetworkMode ( networkName ) . IsUserDefined ( ) {
// Networks that are not user defined always exist on all nodes as
// local-scoped networks, so there's no need to inspect them.
continue
}
2017-09-26 12:50:09 +00:00
network , err := cli . NetworkInspect ( ctx , networkName , types . NetworkInspectOptions { } )
switch {
case client . IsErrNotFound ( err ) :
return errors . Format ( "network %q is declared as external, but could not be found. You need to create a swarm-scoped network before the stack is deployed" , networkName )
case err != nil :
return err
case network . Scope != "swarm" :
return errors . Format ( "network %q is declared as external, but it is not in the right scope: %q instead of \"swarm\"" , networkName , network . Scope )
}
}
return nil
}
func createNetworks ( ctx context . Context , cli * client . Client , namespace compose . Namespace , networks map [ string ] types . NetworkCreate ) error {
opts := types . NetworkListOptions {
Filters : filters . NewArgs ( ) ,
}
opts . Filters . Add ( "label" , stackLabel + "=" + namespace . Name ( ) )
existingNetworks , err := cli . NetworkList ( ctx , opts )
if err != nil {
return err
}
existingNetworkMap := make ( map [ string ] types . NetworkResource )
for _ , network := range existingNetworks {
existingNetworkMap [ network . Name ] = network
}
for internalName , createOpts := range networks {
name := namespace . Scope ( internalName )
if _ , exists := existingNetworkMap [ name ] ; exists {
continue
}
if createOpts . Driver == "" {
createOpts . Driver = "overlay"
}
mgr . Logger ( ) . Infof ( "Creating network %s" , name )
if _ , err = cli . NetworkCreate ( ctx , name , createOpts ) ; err != nil {
return errors . Wrap ( "failed to create network " + internalName , err )
}
}
return nil
}
func createSecrets ( ctx context . Context , cli * client . Client , secrets [ ] swarm . SecretSpec ) error {
for _ , secretSpec := range secrets {
secret , _ , err := cli . SecretInspectWithRaw ( ctx , secretSpec . Name )
switch {
case err == nil :
// secret already exists, then we update that
if err = cli . SecretUpdate ( ctx , secret . ID , secret . Meta . Version , secretSpec ) ; err != nil {
return errors . Wrap ( "failed to update secret " + secretSpec . Name , err )
}
case client . IsErrSecretNotFound ( err ) :
// secret does not exist, then we create a new one.
if _ , err = cli . SecretCreate ( ctx , secretSpec ) ; err != nil {
return errors . Wrap ( "failed to create secret " + secretSpec . Name , err )
}
default :
return err
}
}
return nil
}
func createConfigs ( ctx context . Context , cli * client . Client , configs [ ] swarm . ConfigSpec ) error {
for _ , configSpec := range configs {
config , _ , err := cli . ConfigInspectWithRaw ( ctx , configSpec . Name )
switch {
case err == nil :
// config already exists, then we update that
if err = cli . ConfigUpdate ( ctx , config . ID , config . Meta . Version , configSpec ) ; err != nil {
errors . Wrap ( "failed to update config " + configSpec . Name , err )
}
case client . IsErrConfigNotFound ( err ) :
// config does not exist, then we create a new one.
if _ , err = cli . ConfigCreate ( ctx , configSpec ) ; err != nil {
errors . Wrap ( "failed to create config " + configSpec . Name , err )
}
default :
return err
}
}
return nil
}
func getServices (
ctx context . Context ,
cli * client . Client ,
namespace string ,
) ( [ ] swarm . Service , error ) {
opts := types . ServiceListOptions {
Filters : filters . NewArgs ( ) ,
}
opts . Filters . Add ( "label" , stackLabel + "=" + namespace )
return cli . ServiceList ( ctx , opts )
}
func deployServices (
ctx context . Context ,
cli * client . Client ,
services map [ string ] swarm . ServiceSpec ,
namespace compose . Namespace ,
authes map [ string ] string ,
//sendAuth bool,
//resolveImage string,
) error {
existingServices , err := getServices ( ctx , cli , namespace . Name ( ) )
if err != nil {
return err
}
existingServiceMap := make ( map [ string ] swarm . Service )
for _ , service := range existingServices {
existingServiceMap [ service . Spec . Name ] = service
}
for internalName , serviceSpec := range services {
name := namespace . Scope ( internalName )
// TODO: Add auth
encodedAuth := authes [ serviceSpec . TaskTemplate . ContainerSpec . Image ]
//image := serviceSpec.TaskTemplate.ContainerSpec.Image
//if sendAuth {
// // Retrieve encoded auth token from the image reference
// encodedAuth, err = command.RetrieveAuthTokenFromImage(ctx, dockerCli, image)
// if err != nil {
// return err
// }
//}
if service , exists := existingServiceMap [ name ] ; exists {
mgr . Logger ( ) . Infof ( "Updating service %s (id: %s)" , name , service . ID )
updateOpts := types . ServiceUpdateOptions {
RegistryAuthFrom : types . RegistryAuthFromSpec ,
EncodedRegistryAuth : encodedAuth ,
}
//if resolveImage == resolveImageAlways || (resolveImage == resolveImageChanged && image != service.Spec.Labels[compose.LabelImage]) {
// updateOpts.QueryRegistry = true
//}
response , err := cli . ServiceUpdate (
ctx ,
service . ID ,
service . Version ,
serviceSpec ,
updateOpts ,
)
if err != nil {
return errors . Wrap ( "failed to update service " + name , err )
}
for _ , warning := range response . Warnings {
mgr . Logger ( ) . Warn ( warning )
}
} else {
mgr . Logger ( ) . Infof ( "Creating service %s" , name )
createOpts := types . ServiceCreateOptions { EncodedRegistryAuth : encodedAuth }
// query registry if flag disabling it was not set
//if resolveImage == resolveImageAlways || resolveImage == resolveImageChanged {
// createOpts.QueryRegistry = true
//}
if _ , err = cli . ServiceCreate ( ctx , serviceSpec , createOpts ) ; err != nil {
return errors . Wrap ( "failed to create service " + name , err )
}
}
}
return nil
}