2021-10-21 17:54:43 +00:00
package stack // https://github.com/docker/cli/blob/master/cli/command/stack/swarm/common.go
2021-08-03 07:49:16 +00:00
import (
"context"
2021-10-12 22:24:23 +00:00
"fmt"
2021-11-10 08:06:55 +00:00
"io"
"io/ioutil"
2021-08-03 07:49:16 +00:00
"strings"
2021-11-14 22:15:35 +00:00
"time"
2021-08-03 07:49:16 +00:00
2021-10-21 17:35:13 +00:00
"coopcloud.tech/abra/pkg/upstream/convert"
2021-11-10 08:06:55 +00:00
"github.com/docker/cli/cli/command/service/progress"
2023-02-08 18:53:04 +00:00
"github.com/docker/cli/cli/command/stack/formatter"
2021-09-01 11:08:42 +00:00
composetypes "github.com/docker/cli/cli/compose/types"
2021-08-03 07:49:16 +00:00
"github.com/docker/docker/api/types"
2021-09-01 11:08:42 +00:00
"github.com/docker/docker/api/types/container"
2021-08-03 07:49:16 +00:00
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
2021-09-01 11:08:42 +00:00
"github.com/docker/docker/api/types/versions"
2021-08-03 07:49:16 +00:00
"github.com/docker/docker/client"
2023-01-31 15:09:09 +00:00
dockerClient "github.com/docker/docker/client"
2021-09-01 11:08:42 +00:00
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
2021-08-03 07:49:16 +00:00
)
2021-09-04 19:23:47 +00:00
// Resolve image constants
const (
defaultNetworkDriver = "overlay"
ResolveImageAlways = "always"
ResolveImageChanged = "changed"
ResolveImageNever = "never"
)
2021-08-03 07:49:16 +00:00
type StackStatus struct {
Services [ ] swarm . Service
Err error
}
func getStackFilter ( namespace string ) filters . Args {
filter := filters . NewArgs ( )
filter . Add ( "label" , convert . LabelNamespace + "=" + namespace )
return filter
}
func getStackServiceFilter ( namespace string ) filters . Args {
return getStackFilter ( namespace )
}
func getAllStacksFilter ( ) filters . Args {
filter := filters . NewArgs ( )
filter . Add ( "label" , convert . LabelNamespace )
return filter
}
2021-09-08 10:55:33 +00:00
func GetStackServices ( ctx context . Context , dockerclient client . APIClient , namespace string ) ( [ ] swarm . Service , error ) {
2021-09-04 19:12:53 +00:00
return dockerclient . ServiceList ( ctx , types . ServiceListOptions { Filters : getStackServiceFilter ( namespace ) } )
2021-08-03 07:49:16 +00:00
}
2021-08-25 11:06:49 +00:00
// GetDeployedServicesByLabel filters services by label
2023-01-31 15:09:09 +00:00
func GetDeployedServicesByLabel ( cl * dockerClient . Client , contextName string , label string ) StackStatus {
2021-08-25 11:06:49 +00:00
filters := filters . NewArgs ( )
filters . Add ( "label" , label )
2023-01-31 15:09:09 +00:00
services , err := cl . ServiceList ( context . Background ( ) , types . ServiceListOptions { Filters : filters } )
2021-08-25 11:06:49 +00:00
if err != nil {
return StackStatus { [ ] swarm . Service { } , err }
}
return StackStatus { services , nil }
}
2023-01-31 15:09:09 +00:00
func GetAllDeployedServices ( cl * dockerClient . Client , contextName string ) StackStatus {
services , err := cl . ServiceList ( context . Background ( ) , types . ServiceListOptions { Filters : getAllStacksFilter ( ) } )
2021-08-03 07:49:16 +00:00
if err != nil {
return StackStatus { [ ] swarm . Service { } , err }
}
2021-08-25 11:06:49 +00:00
2021-08-03 07:49:16 +00:00
return StackStatus { services , nil }
}
2021-10-14 09:29:57 +00:00
// GetDeployedServicesByName filters services by name
2023-01-31 15:09:09 +00:00
func GetDeployedServicesByName ( ctx context . Context , cl * dockerClient . Client , stackName , serviceName string ) StackStatus {
2021-10-14 09:29:57 +00:00
filters := filters . NewArgs ( )
2021-12-09 13:12:06 +00:00
filters . Add ( "name" , fmt . Sprintf ( "%s_%s" , stackName , serviceName ) )
2021-10-14 09:29:57 +00:00
services , err := cl . ServiceList ( ctx , types . ServiceListOptions { Filters : filters } )
if err != nil {
return StackStatus { [ ] swarm . Service { } , err }
}
return StackStatus { services , nil }
}
2021-10-12 22:24:23 +00:00
// IsDeployed chekcks whether an appp is deployed or not.
2023-01-31 15:09:09 +00:00
func IsDeployed ( ctx context . Context , cl * dockerClient . Client , stackName string ) ( bool , string , error ) {
2022-01-01 16:23:21 +00:00
version := "unknown"
2021-10-12 22:24:23 +00:00
isDeployed := false
filter := filters . NewArgs ( )
filter . Add ( "label" , fmt . Sprintf ( "%s=%s" , convert . LabelNamespace , stackName ) )
services , err := cl . ServiceList ( ctx , types . ServiceListOptions { Filters : filter } )
if err != nil {
return false , version , err
}
if len ( services ) > 0 {
for _ , service := range services {
labelKey := fmt . Sprintf ( "coop-cloud.%s.version" , stackName )
if deployedVersion , ok := service . Spec . Labels [ labelKey ] ; ok {
version = deployedVersion
break
}
}
2021-12-23 23:25:45 +00:00
logrus . Debugf ( "%s has been detected as deployed with version %s" , stackName , version )
2021-10-12 22:24:23 +00:00
return true , version , nil
}
2021-12-23 23:25:45 +00:00
logrus . Debugf ( "%s has been detected as not deployed" , stackName )
2021-10-12 22:24:23 +00:00
return isDeployed , version , nil
}
2021-09-01 11:08:42 +00:00
// pruneServices removes services that are no longer referenced in the source
2023-01-31 15:09:09 +00:00
func pruneServices ( ctx context . Context , cl * dockerClient . Client , namespace convert . Namespace , services map [ string ] struct { } ) {
2021-09-08 10:55:33 +00:00
oldServices , err := GetStackServices ( ctx , cl , namespace . Name ( ) )
2021-09-01 11:08:42 +00:00
if err != nil {
logrus . Infof ( "Failed to list services: %s\n" , err )
}
pruneServices := [ ] swarm . Service { }
for _ , service := range oldServices {
if _ , exists := services [ namespace . Descope ( service . Spec . Name ) ] ; ! exists {
pruneServices = append ( pruneServices , service )
}
}
removeServices ( ctx , cl , pruneServices )
}
// RunDeploy is the swarm implementation of docker stack deploy
2023-01-31 15:09:09 +00:00
func RunDeploy ( cl * dockerClient . Client , opts Deploy , cfg * composetypes . Config , appName string , dontWait bool ) error {
2021-09-01 11:08:42 +00:00
if err := validateResolveImageFlag ( & opts ) ; err != nil {
return err
}
// client side image resolution should not be done when the supported
// server version is older than 1.30
if versions . LessThan ( cl . ClientVersion ( ) , "1.30" ) {
opts . ResolveImage = ResolveImageNever
}
2023-01-31 15:09:09 +00:00
return deployCompose ( context . Background ( ) , cl , opts , cfg , appName , dontWait )
2021-09-01 11:08:42 +00:00
}
// validateResolveImageFlag validates the opts.resolveImage command line option
2021-09-04 19:08:14 +00:00
func validateResolveImageFlag ( opts * Deploy ) error {
2021-09-01 11:08:42 +00:00
switch opts . ResolveImage {
case ResolveImageAlways , ResolveImageChanged , ResolveImageNever :
return nil
default :
return errors . Errorf ( "Invalid option %s for flag --resolve-image" , opts . ResolveImage )
}
}
2023-01-31 15:09:09 +00:00
func deployCompose ( ctx context . Context , cl * dockerClient . Client , opts Deploy , config * composetypes . Config , appName string , dontWait bool ) error {
2021-09-01 11:08:42 +00:00
namespace := convert . NewNamespace ( opts . Namespace )
if opts . Prune {
services := map [ string ] struct { } { }
for _ , service := range config . Services {
services [ service . Name ] = struct { } { }
}
pruneServices ( ctx , cl , namespace , services )
}
serviceNetworks := getServicesDeclaredNetworks ( config . Services )
networks , externalNetworks := convert . Networks ( namespace , config . Networks , serviceNetworks )
if err := validateExternalNetworks ( ctx , cl , externalNetworks ) ; err != nil {
return err
}
if err := createNetworks ( ctx , cl , namespace , networks ) ; err != nil {
return err
}
secrets , err := convert . Secrets ( namespace , config . Secrets )
if err != nil {
return err
}
if err := createSecrets ( ctx , cl , secrets ) ; err != nil {
return err
}
configs , err := convert . Configs ( namespace , config . Configs )
if err != nil {
return err
}
if err := createConfigs ( ctx , cl , configs ) ; err != nil {
return err
}
services , err := convert . Services ( namespace , config , cl )
if err != nil {
return err
}
2021-09-08 10:55:33 +00:00
2022-01-01 16:22:19 +00:00
return deployServices ( ctx , cl , services , namespace , opts . SendRegistryAuth , opts . ResolveImage , appName , dontWait )
2021-09-01 11:08:42 +00: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
}
2023-01-31 15:09:09 +00:00
func validateExternalNetworks ( ctx context . Context , client dockerClient . NetworkAPIClient , externalNetworks [ ] string ) error {
2021-09-01 11:08:42 +00:00
for _ , networkName := range externalNetworks {
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
}
network , err := client . NetworkInspect ( ctx , networkName , types . NetworkInspectOptions { } )
switch {
2023-01-31 15:09:09 +00:00
case dockerClient . IsErrNotFound ( err ) :
2021-09-01 11:08:42 +00:00
return errors . 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 )
case err != nil :
return err
case network . Scope != "swarm" :
return errors . Errorf ( "network %q is declared as external, but it is not in the right scope: %q instead of \"swarm\"" , networkName , network . Scope )
}
}
return nil
}
2023-01-31 15:09:09 +00:00
func createSecrets ( ctx context . Context , cl * dockerClient . Client , secrets [ ] swarm . SecretSpec ) error {
2021-09-01 11:08:42 +00:00
for _ , secretSpec := range secrets {
secret , _ , err := cl . SecretInspectWithRaw ( ctx , secretSpec . Name )
switch {
case err == nil :
// secret already exists, then we update that
if err := cl . SecretUpdate ( ctx , secret . ID , secret . Meta . Version , secretSpec ) ; err != nil {
return errors . Wrapf ( err , "failed to update secret %s" , secretSpec . Name )
}
2023-01-31 15:09:09 +00:00
case dockerClient . IsErrNotFound ( err ) :
2021-09-01 11:08:42 +00:00
// secret does not exist, then we create a new one.
logrus . Infof ( "Creating secret %s\n" , secretSpec . Name )
if _ , err := cl . SecretCreate ( ctx , secretSpec ) ; err != nil {
return errors . Wrapf ( err , "failed to create secret %s" , secretSpec . Name )
}
default :
return err
}
}
return nil
}
2023-01-31 15:09:09 +00:00
func createConfigs ( ctx context . Context , cl * dockerClient . Client , configs [ ] swarm . ConfigSpec ) error {
2021-09-01 11:08:42 +00:00
for _ , configSpec := range configs {
config , _ , err := cl . ConfigInspectWithRaw ( ctx , configSpec . Name )
switch {
case err == nil :
// config already exists, then we update that
if err := cl . ConfigUpdate ( ctx , config . ID , config . Meta . Version , configSpec ) ; err != nil {
return errors . Wrapf ( err , "failed to update config %s" , configSpec . Name )
}
2023-01-31 15:09:09 +00:00
case dockerClient . IsErrNotFound ( err ) :
2021-09-01 11:08:42 +00:00
// config does not exist, then we create a new one.
logrus . Infof ( "Creating config %s\n" , configSpec . Name )
if _ , err := cl . ConfigCreate ( ctx , configSpec ) ; err != nil {
return errors . Wrapf ( err , "failed to create config %s" , configSpec . Name )
}
default :
return err
}
}
return nil
}
2023-01-31 15:09:09 +00:00
func createNetworks ( ctx context . Context , cl * dockerClient . Client , namespace convert . Namespace , networks map [ string ] types . NetworkCreate ) error {
2021-09-01 11:08:42 +00:00
existingNetworks , err := getStackNetworks ( ctx , cl , namespace . Name ( ) )
if err != nil {
return err
}
existingNetworkMap := make ( map [ string ] types . NetworkResource )
for _ , network := range existingNetworks {
existingNetworkMap [ network . Name ] = network
}
for name , createOpts := range networks {
if _ , exists := existingNetworkMap [ name ] ; exists {
continue
}
if createOpts . Driver == "" {
createOpts . Driver = defaultNetworkDriver
}
logrus . Infof ( "Creating network %s\n" , name )
if _ , err := cl . NetworkCreate ( ctx , name , createOpts ) ; err != nil {
return errors . Wrapf ( err , "failed to create network %s" , name )
}
}
2021-08-03 07:49:16 +00:00
return nil
}
2021-09-01 11:08:42 +00:00
2021-09-04 19:23:47 +00:00
func deployServices (
ctx context . Context ,
2023-01-31 15:09:09 +00:00
cl * dockerClient . Client ,
2021-09-04 19:23:47 +00:00
services map [ string ] swarm . ServiceSpec ,
namespace convert . Namespace ,
sendAuth bool ,
2021-12-21 22:48:32 +00:00
resolveImage string ,
2022-01-01 16:22:19 +00:00
appName string ,
2021-12-24 01:23:46 +00:00
dontWait bool ) error {
2021-09-08 10:55:33 +00:00
existingServices , err := GetStackServices ( ctx , cl , namespace . Name ( ) )
2021-09-01 11:08:42 +00:00
if err != nil {
return err
}
existingServiceMap := make ( map [ string ] swarm . Service )
for _ , service := range existingServices {
existingServiceMap [ service . Spec . Name ] = service
}
2021-11-26 20:24:15 +00:00
serviceIDs := make ( map [ string ] string )
2021-09-01 11:08:42 +00:00
for internalName , serviceSpec := range services {
var (
name = namespace . Scope ( internalName )
image = serviceSpec . TaskTemplate . ContainerSpec . Image
encodedAuth string
)
if service , exists := existingServiceMap [ name ] ; exists {
logrus . Infof ( "Updating service %s (id: %s)\n" , name , service . ID )
updateOpts := types . ServiceUpdateOptions { EncodedRegistryAuth : encodedAuth }
switch resolveImage {
case ResolveImageAlways :
// image should be updated by the server using QueryRegistry
updateOpts . QueryRegistry = true
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
}
}
// Stack deploy does not have a `--force` option. Preserve existing
// ForceUpdate value so that tasks are not re-deployed if not updated.
serviceSpec . TaskTemplate . ForceUpdate = service . Spec . TaskTemplate . ForceUpdate
response , err := cl . ServiceUpdate ( ctx , service . ID , service . Version , serviceSpec , updateOpts )
if err != nil {
return errors . Wrapf ( err , "failed to update service %s" , name )
}
2021-11-26 20:24:15 +00:00
serviceIDs [ service . ID ] = name
2021-11-10 08:06:55 +00:00
2021-09-01 11:08:42 +00:00
for _ , warning := range response . Warnings {
logrus . Warn ( warning )
}
} else {
logrus . Infof ( "Creating service %s\n" , name )
createOpts := types . ServiceCreateOptions { EncodedRegistryAuth : encodedAuth }
// query registry if flag disabling it was not set
if resolveImage == ResolveImageAlways || resolveImage == ResolveImageChanged {
createOpts . QueryRegistry = true
}
2021-11-10 08:06:55 +00:00
serviceCreateResponse , err := cl . ServiceCreate ( ctx , serviceSpec , createOpts )
if err != nil {
2021-09-01 11:08:42 +00:00
return errors . Wrapf ( err , "failed to create service %s" , name )
}
2021-11-10 08:06:55 +00:00
2021-11-26 20:24:15 +00:00
serviceIDs [ serviceCreateResponse . ID ] = name
2021-09-01 11:08:42 +00:00
}
}
2021-11-10 08:06:55 +00:00
2021-11-26 20:24:15 +00:00
var serviceNames [ ] string
for _ , serviceName := range serviceIDs {
serviceNames = append ( serviceNames , serviceName )
}
2021-11-10 08:06:55 +00:00
2021-12-24 01:23:46 +00:00
if dontWait {
logrus . Warn ( "skipping converge logic checks" )
return nil
}
logrus . Infof ( "waiting for services to converge: %s" , strings . Join ( serviceNames , ", " ) )
2021-11-10 08:06:55 +00:00
ch := make ( chan error , len ( serviceIDs ) )
2021-11-26 20:24:15 +00:00
for serviceID , serviceName := range serviceIDs {
logrus . Debugf ( "waiting on %s to converge" , serviceName )
2022-01-01 16:22:19 +00:00
go func ( sID , sName , aName string ) {
ch <- WaitOnService ( ctx , cl , sID , aName )
} ( serviceID , serviceName , appName )
2021-11-10 08:06:55 +00:00
}
for _ , serviceID := range serviceIDs {
err := <- ch
if err != nil {
return err
}
logrus . Debugf ( "assuming %s converged successfully" , serviceID )
}
2021-12-24 00:32:42 +00:00
logrus . Info ( "services converged 👌" )
2021-09-01 11:08:42 +00:00
return nil
}
2021-09-04 19:12:53 +00:00
func getStackNetworks ( ctx context . Context , dockerclient client . APIClient , namespace string ) ( [ ] types . NetworkResource , error ) {
return dockerclient . NetworkList ( ctx , types . NetworkListOptions { Filters : getStackFilter ( namespace ) } )
2021-09-01 11:08:42 +00:00
}
2021-09-04 19:12:53 +00:00
func getStackSecrets ( ctx context . Context , dockerclient client . APIClient , namespace string ) ( [ ] swarm . Secret , error ) {
return dockerclient . SecretList ( ctx , types . SecretListOptions { Filters : getStackFilter ( namespace ) } )
2021-09-01 11:08:42 +00:00
}
2021-09-04 19:12:53 +00:00
func getStackConfigs ( ctx context . Context , dockerclient client . APIClient , namespace string ) ( [ ] swarm . Config , error ) {
return dockerclient . ConfigList ( ctx , types . ConfigListOptions { Filters : getStackFilter ( namespace ) } )
2021-09-01 11:08:42 +00:00
}
2021-11-10 08:06:55 +00:00
// https://github.com/docker/cli/blob/master/cli/command/service/helpers.go
// https://github.com/docker/cli/blob/master/cli/command/service/progress/progress.go
2023-01-31 15:09:09 +00:00
func WaitOnService ( ctx context . Context , cl * dockerClient . Client , serviceID , appName string ) error {
2021-11-10 08:06:55 +00:00
errChan := make ( chan error , 1 )
pipeReader , pipeWriter := io . Pipe ( )
go func ( ) {
errChan <- progress . ServiceProgress ( ctx , cl , serviceID , pipeWriter )
} ( )
go io . Copy ( ioutil . Discard , pipeReader )
2021-11-14 22:15:35 +00:00
2022-01-03 15:33:18 +00:00
timeout := 50 * time . Second
2021-11-14 22:15:35 +00:00
select {
case err := <- errChan :
return err
case <- time . After ( timeout ) :
2021-12-21 22:48:32 +00:00
return fmt . Errorf ( fmt . Sprintf ( `
2022-01-03 15:33:28 +00:00
% s has not converged ( % s second timeout reached ) .
2021-12-21 22:48:32 +00:00
This does not necessarily mean your deployment has failed , it may just be that
the app is taking longer to deploy based on your server resources or network
2022-01-03 15:33:28 +00:00
latency .
You can track latest deployment status with :
abra app ps -- watch % s
And inspect the logs with :
2021-12-21 22:48:32 +00:00
abra app logs % s
2022-01-03 15:33:28 +00:00
If a service is failing to even start , try smoke out the error with :
2021-12-21 22:48:32 +00:00
2022-01-01 16:22:19 +00:00
abra app errors -- watch % s
2021-12-21 22:48:32 +00:00
2022-01-01 16:22:19 +00:00
` , appName , timeout , appName , appName , appName ) )
2021-11-14 22:15:35 +00:00
}
2021-11-10 08:06:55 +00:00
}
2023-02-08 18:53:04 +00:00
// Copypasta from https://github.com/docker/cli/blob/master/cli/command/stack/swarm/list.go
// GetStacks lists the swarm stacks.
func GetStacks ( cl * dockerClient . Client ) ( [ ] * formatter . Stack , error ) {
services , err := cl . ServiceList (
context . Background ( ) ,
types . ServiceListOptions { Filters : getAllStacksFilter ( ) } )
if err != nil {
return nil , err
}
m := make ( map [ string ] * formatter . Stack )
for _ , service := range services {
labels := service . Spec . Labels
name , ok := labels [ convert . LabelNamespace ]
if ! ok {
return nil , errors . Errorf ( "cannot get label %s for service %s" ,
convert . LabelNamespace , service . ID )
}
ztack , ok := m [ name ]
if ! ok {
m [ name ] = & formatter . Stack {
Name : name ,
Services : 1 ,
}
} else {
ztack . Services ++
}
}
var stacks [ ] * formatter . Stack
for _ , stack := range m {
stacks = append ( stacks , stack )
}
return stacks , nil
}