2017-11-20 09:30:52 -05:00
package swarm
2017-02-17 03:28:08 -05:00
import (
2018-05-03 21:02:44 -04:00
"context"
2023-05-05 10:00:30 -04:00
"errors"
2017-02-17 03:28:08 -05:00
"fmt"
2017-04-17 18:07:56 -04:00
"github.com/docker/cli/cli/command"
2023-05-05 10:00:30 -04:00
servicecli "github.com/docker/cli/cli/command/service"
2017-12-04 06:30:39 -05:00
"github.com/docker/cli/cli/command/stack/options"
2017-04-17 18:07:56 -04:00
"github.com/docker/cli/cli/compose/convert"
composetypes "github.com/docker/cli/cli/compose/types"
2017-02-17 03:28:08 -05:00
"github.com/docker/docker/api/types"
2017-05-25 22:50:08 -04:00
"github.com/docker/docker/api/types/container"
2017-02-17 03:28:08 -05:00
"github.com/docker/docker/api/types/swarm"
2017-05-08 13:51:30 -04:00
apiclient "github.com/docker/docker/client"
2023-05-10 08:43:18 -04:00
"github.com/docker/docker/errdefs"
2017-02-17 03:28:08 -05:00
)
2023-05-05 10:00:30 -04:00
func deployCompose ( ctx context . Context , dockerCli command . Cli , opts * options . Deploy , config * composetypes . Config ) error {
2017-02-17 03:28:08 -05:00
if err := checkDaemonIsSwarmManager ( ctx , dockerCli ) ; err != nil {
return err
}
2017-12-04 06:30:39 -05:00
namespace := convert . NewNamespace ( opts . Namespace )
2017-02-17 03:28:08 -05:00
2017-12-04 06:30:39 -05:00
if opts . Prune {
2017-02-22 15:43:13 -05:00
services := map [ string ] struct { } { }
for _ , service := range config . Services {
services [ service . Name ] = struct { } { }
}
pruneServices ( ctx , dockerCli , namespace , services )
}
2017-02-17 03:28:08 -05:00
2017-02-22 15:43:13 -05:00
serviceNetworks := getServicesDeclaredNetworks ( config . Services )
2017-02-17 03:28:08 -05:00
networks , externalNetworks := convert . Networks ( namespace , config . Networks , serviceNetworks )
2017-05-26 12:15:23 -04:00
if err := validateExternalNetworks ( ctx , dockerCli . Client ( ) , externalNetworks ) ; err != nil {
2017-02-17 03:28:08 -05:00
return err
}
if err := createNetworks ( ctx , dockerCli , namespace , networks ) ; err != nil {
return err
}
secrets , err := convert . Secrets ( namespace , config . Secrets )
if err != nil {
return err
}
2017-05-26 12:15:23 -04:00
if err := createSecrets ( ctx , dockerCli , secrets ) ; err != nil {
2017-02-17 03:28:08 -05:00
return err
}
2017-05-15 11:19:32 -04:00
configs , err := convert . Configs ( namespace , config . Configs )
if err != nil {
return err
}
2017-05-26 12:15:23 -04:00
if err := createConfigs ( ctx , dockerCli , configs ) ; err != nil {
2017-05-15 11:19:32 -04:00
return err
}
2023-09-09 18:27:44 -04:00
services , err := convert . Services ( ctx , namespace , config , dockerCli . Client ( ) )
2017-02-17 03:28:08 -05:00
if err != nil {
return err
}
2023-05-05 10:00:30 -04:00
serviceIDs , err := deployServices ( ctx , dockerCli , services , namespace , opts . SendRegistryAuth , opts . ResolveImage )
if err != nil {
return err
}
if opts . Detach {
return nil
}
return waitOnServices ( ctx , dockerCli , serviceIDs , opts . Quiet )
2017-02-17 03:28:08 -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
}
2022-09-02 16:49:01 -04:00
func validateExternalNetworks ( ctx context . Context , client apiclient . NetworkAPIClient , externalNetworks [ ] string ) error {
2017-02-17 03:28:08 -05:00
for _ , networkName := range externalNetworks {
2017-07-19 19:01:13 -04: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-06-12 22:53:53 -04:00
network , err := client . NetworkInspect ( ctx , networkName , types . NetworkInspectOptions { } )
2017-05-26 12:15:23 -04:00
switch {
2023-05-10 08:43:18 -04:00
case errdefs . IsNotFound ( err ) :
2023-05-05 10:00:30 -04:00
return fmt . Errorf ( "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 )
2017-05-26 12:15:23 -04:00
case err != nil :
2017-02-17 03:28:08 -05:00
return err
2017-07-19 19:01:13 -04:00
case network . Scope != "swarm" :
2023-05-05 10:00:30 -04:00
return fmt . Errorf ( "network %q is declared as external, but it is not in the right scope: %q instead of \"swarm\"" , networkName , network . Scope )
2017-02-17 03:28:08 -05:00
}
}
return nil
}
2020-05-20 08:13:14 -04:00
func createSecrets ( ctx context . Context , dockerCli command . Cli , secrets [ ] swarm . SecretSpec ) error {
2017-02-17 03:28:08 -05:00
client := dockerCli . Client ( )
for _ , secretSpec := range secrets {
secret , _ , err := client . SecretInspectWithRaw ( ctx , secretSpec . Name )
2017-06-01 14:50:07 -04:00
switch {
case err == nil :
2017-02-17 03:28:08 -05:00
// secret already exists, then we update that
if err := client . SecretUpdate ( ctx , secret . ID , secret . Meta . Version , secretSpec ) ; err != nil {
2023-05-05 10:00:30 -04:00
return fmt . Errorf ( "failed to update secret %s: %w" , secretSpec . Name , err )
2017-02-17 03:28:08 -05:00
}
2023-05-10 08:43:18 -04:00
case errdefs . IsNotFound ( err ) :
2017-02-17 03:28:08 -05:00
// secret does not exist, then we create a new one.
2017-10-03 05:30:45 -04:00
fmt . Fprintf ( dockerCli . Out ( ) , "Creating secret %s\n" , secretSpec . Name )
2017-02-17 03:28:08 -05:00
if _ , err := client . SecretCreate ( ctx , secretSpec ) ; err != nil {
2023-05-05 10:00:30 -04:00
return fmt . Errorf ( "failed to create secret %s: %w" , secretSpec . Name , err )
2017-02-17 03:28:08 -05:00
}
2017-06-01 14:50:07 -04:00
default :
2017-02-17 03:28:08 -05:00
return err
}
}
return nil
}
2020-05-20 08:13:14 -04:00
func createConfigs ( ctx context . Context , dockerCli command . Cli , configs [ ] swarm . ConfigSpec ) error {
2017-05-15 11:19:32 -04:00
client := dockerCli . Client ( )
for _ , configSpec := range configs {
config , _ , err := client . ConfigInspectWithRaw ( ctx , configSpec . Name )
2017-06-01 14:50:07 -04:00
switch {
case err == nil :
2017-05-15 11:19:32 -04:00
// config already exists, then we update that
if err := client . ConfigUpdate ( ctx , config . ID , config . Meta . Version , configSpec ) ; err != nil {
2023-05-05 10:00:30 -04:00
return fmt . Errorf ( "failed to update config %s: %w" , configSpec . Name , err )
2017-05-15 11:19:32 -04:00
}
2023-05-10 08:43:18 -04:00
case errdefs . IsNotFound ( err ) :
2017-05-15 11:19:32 -04:00
// config does not exist, then we create a new one.
2017-10-03 05:30:45 -04:00
fmt . Fprintf ( dockerCli . Out ( ) , "Creating config %s\n" , configSpec . Name )
2017-05-15 11:19:32 -04:00
if _ , err := client . ConfigCreate ( ctx , configSpec ) ; err != nil {
2023-05-05 10:00:30 -04:00
return fmt . Errorf ( "failed to create config %s: %w" , configSpec . Name , err )
2017-05-15 11:19:32 -04:00
}
2017-06-01 14:50:07 -04:00
default :
2017-05-15 11:19:32 -04:00
return err
}
}
return nil
}
2020-05-20 08:13:14 -04:00
func createNetworks ( ctx context . Context , dockerCli command . Cli , namespace convert . Namespace , networks map [ string ] types . NetworkCreate ) error {
2017-02-17 03:28:08 -05:00
client := dockerCli . Client ( )
existingNetworks , err := getStackNetworks ( ctx , client , namespace . Name ( ) )
if err != nil {
return err
}
existingNetworkMap := make ( map [ string ] types . NetworkResource )
for _ , network := range existingNetworks {
existingNetworkMap [ network . Name ] = network
}
2018-03-12 13:09:59 -04:00
for name , createOpts := range networks {
2017-02-17 03:28:08 -05:00
if _ , exists := existingNetworkMap [ name ] ; exists {
continue
}
if createOpts . Driver == "" {
createOpts . Driver = defaultNetworkDriver
}
fmt . Fprintf ( dockerCli . Out ( ) , "Creating network %s\n" , name )
if _ , err := client . NetworkCreate ( ctx , name , createOpts ) ; err != nil {
2023-05-05 10:00:30 -04:00
return fmt . Errorf ( "failed to create network %s: %w" , name , err )
2017-02-17 03:28:08 -05:00
}
}
return nil
}
2023-05-05 10:00:30 -04:00
func deployServices ( ctx context . Context , dockerCli command . Cli , services map [ string ] swarm . ServiceSpec , namespace convert . Namespace , sendAuth bool , resolveImage string ) ( [ ] string , error ) {
2017-02-17 03:28:08 -05:00
apiClient := dockerCli . Client ( )
out := dockerCli . Out ( )
2018-05-28 06:26:14 -04:00
existingServices , err := getStackServices ( ctx , apiClient , namespace . Name ( ) )
2017-02-17 03:28:08 -05:00
if err != nil {
2023-05-05 10:00:30 -04:00
return nil , err
2017-02-17 03:28:08 -05:00
}
existingServiceMap := make ( map [ string ] swarm . Service )
for _ , service := range existingServices {
existingServiceMap [ service . Spec . Name ] = service
}
2023-05-05 10:00:30 -04:00
var serviceIDs [ ] string
2017-02-17 03:28:08 -05:00
for internalName , serviceSpec := range services {
2020-05-20 08:01:39 -04:00
var (
name = namespace . Scope ( internalName )
image = serviceSpec . TaskTemplate . ContainerSpec . Image
encodedAuth string
)
2017-02-17 03:28:08 -05:00
if sendAuth {
// Retrieve encoded auth token from the image reference
2023-07-10 11:24:07 -04:00
encodedAuth , err = command . RetrieveAuthTokenFromImage ( dockerCli . ConfigFile ( ) , image )
2017-02-17 03:28:08 -05:00
if err != nil {
2023-05-05 10:00:30 -04:00
return nil , err
2017-02-17 03:28:08 -05:00
}
}
if service , exists := existingServiceMap [ name ] ; exists {
fmt . Fprintf ( out , "Updating service %s (id: %s)\n" , name , service . ID )
2017-05-22 17:06:36 -04:00
updateOpts := types . ServiceUpdateOptions { EncodedRegistryAuth : encodedAuth }
2020-05-20 08:01:39 -04:00
switch resolveImage {
case ResolveImageAlways :
2017-07-26 09:33:57 -04:00
// image should be updated by the server using QueryRegistry
2017-06-02 19:21:41 -04:00
updateOpts . QueryRegistry = true
2020-05-20 08:01:39 -04:00
case ResolveImageChanged :
if image != service . Spec . Labels [ convert . LabelImage ] {
// Query the registry to resolve digest for the updated image
updateOpts . QueryRegistry = true
} else {
// image has not changed; update the serviceSpec with the
// existing information that was set by QueryRegistry on the
// previous deploy. Otherwise this will trigger an incorrect
// service update.
serviceSpec . TaskTemplate . ContainerSpec . Image = service . Spec . TaskTemplate . ContainerSpec . Image
}
default :
if image == service . Spec . Labels [ convert . LabelImage ] {
// image has not changed; update the serviceSpec with the
// existing information that was set by QueryRegistry on the
// previous deploy. Otherwise this will trigger an incorrect
// service update.
serviceSpec . TaskTemplate . ContainerSpec . Image = service . Spec . TaskTemplate . ContainerSpec . Image
}
2017-05-22 17:06:36 -04:00
}
2018-03-26 09:24:28 -04:00
2020-05-20 08:01:39 -04:00
// Stack deploy does not have a `--force` option. Preserve existing
// ForceUpdate value so that tasks are not re-deployed if not updated.
2018-03-26 09:24:28 -04:00
// TODO move this to API client?
serviceSpec . TaskTemplate . ForceUpdate = service . Spec . TaskTemplate . ForceUpdate
2020-05-20 08:13:14 -04:00
response , err := apiClient . ServiceUpdate ( ctx , service . ID , service . Version , serviceSpec , updateOpts )
2017-02-17 03:28:08 -05:00
if err != nil {
2023-05-05 10:00:30 -04:00
return nil , fmt . Errorf ( "failed to update service %s: %w" , name , err )
2017-02-17 03:28:08 -05:00
}
for _ , warning := range response . Warnings {
fmt . Fprintln ( dockerCli . Err ( ) , warning )
}
2023-05-05 10:00:30 -04:00
serviceIDs = append ( serviceIDs , service . ID )
2017-02-17 03:28:08 -05:00
} else {
fmt . Fprintf ( out , "Creating service %s\n" , name )
2017-06-01 14:50:07 -04:00
createOpts := types . ServiceCreateOptions { EncodedRegistryAuth : encodedAuth }
2017-05-22 17:06:36 -04:00
// query registry if flag disabling it was not set
2017-12-04 06:30:39 -05:00
if resolveImage == ResolveImageAlways || resolveImage == ResolveImageChanged {
2017-05-22 17:06:36 -04:00
createOpts . QueryRegistry = true
}
2023-05-05 10:00:30 -04:00
response , err := apiClient . ServiceCreate ( ctx , serviceSpec , createOpts )
if err != nil {
return nil , fmt . Errorf ( "failed to create service %s: %w" , name , err )
2017-02-17 03:28:08 -05:00
}
2023-05-05 10:00:30 -04:00
serviceIDs = append ( serviceIDs , response . ID )
}
}
return serviceIDs , nil
}
func waitOnServices ( ctx context . Context , dockerCli command . Cli , serviceIDs [ ] string , quiet bool ) error {
var errs [ ] error
for _ , serviceID := range serviceIDs {
if err := servicecli . WaitOnService ( ctx , dockerCli , serviceID , quiet ) ; err != nil {
errs = append ( errs , fmt . Errorf ( "%s: %w" , serviceID , err ) )
2017-02-17 03:28:08 -05:00
}
}
2023-05-05 10:00:30 -04:00
if len ( errs ) > 0 {
return errors . Join ( errs ... )
}
2017-02-17 03:28:08 -05:00
return nil
}