forked from toolshed/abra
@ -3,20 +3,20 @@ package stack // https://github.com/docker/cli/blob/master/cli/command/stack/swa
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
stdlibErr "errors"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"coopcloud.tech/abra/pkg/ui"
|
||||
"coopcloud.tech/abra/pkg/upstream/convert"
|
||||
"github.com/docker/cli/cli/command/service/progress"
|
||||
"github.com/docker/cli/cli/command/stack/formatter"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
@ -177,7 +177,7 @@ func IsDeployed(ctx context.Context, cl *dockerClient.Client, stackName string)
|
||||
func pruneServices(ctx context.Context, cl *dockerClient.Client, namespace convert.Namespace, services map[string]struct{}) {
|
||||
oldServices, err := GetStackServices(ctx, cl, namespace.Name())
|
||||
if err != nil {
|
||||
log.Infof("failed to list services: %s", err)
|
||||
log.Warnf("failed to list services: %s", err)
|
||||
}
|
||||
|
||||
pruneServices := []swarm.Service{}
|
||||
@ -191,7 +191,17 @@ func pruneServices(ctx context.Context, cl *dockerClient.Client, namespace conve
|
||||
}
|
||||
|
||||
// RunDeploy is the swarm implementation of docker stack deploy
|
||||
func RunDeploy(cl *dockerClient.Client, opts Deploy, cfg *composetypes.Config, appName string, dontWait bool) error {
|
||||
func RunDeploy(
|
||||
cl *dockerClient.Client,
|
||||
opts Deploy,
|
||||
cfg *composetypes.Config,
|
||||
appName string,
|
||||
serverName string,
|
||||
dontWait bool,
|
||||
filters filters.Args,
|
||||
) error {
|
||||
log.Info("initialising deployment")
|
||||
|
||||
if err := validateResolveImageFlag(&opts); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -201,7 +211,16 @@ func RunDeploy(cl *dockerClient.Client, opts Deploy, cfg *composetypes.Config, a
|
||||
opts.ResolveImage = ResolveImageNever
|
||||
}
|
||||
|
||||
return deployCompose(context.Background(), cl, opts, cfg, appName, dontWait)
|
||||
return deployCompose(
|
||||
context.Background(),
|
||||
cl,
|
||||
opts,
|
||||
cfg,
|
||||
appName,
|
||||
serverName,
|
||||
dontWait,
|
||||
filters,
|
||||
)
|
||||
}
|
||||
|
||||
// validateResolveImageFlag validates the opts.resolveImage command line option
|
||||
@ -214,7 +233,16 @@ func validateResolveImageFlag(opts *Deploy) error {
|
||||
}
|
||||
}
|
||||
|
||||
func deployCompose(ctx context.Context, cl *dockerClient.Client, opts Deploy, config *composetypes.Config, appName string, dontWait bool) error {
|
||||
func deployCompose(
|
||||
ctx context.Context,
|
||||
cl *dockerClient.Client,
|
||||
opts Deploy,
|
||||
config *composetypes.Config,
|
||||
appName string,
|
||||
serverName string,
|
||||
dontWait bool,
|
||||
filters filters.Args,
|
||||
) error {
|
||||
namespace := convert.NewNamespace(opts.Namespace)
|
||||
|
||||
if opts.Prune {
|
||||
@ -255,7 +283,14 @@ func deployCompose(ctx context.Context, cl *dockerClient.Client, opts Deploy, co
|
||||
return err
|
||||
}
|
||||
|
||||
serviceIDs, err := deployServices(ctx, cl, services, namespace, opts.SendRegistryAuth, opts.ResolveImage)
|
||||
serviceIDs, err := deployServices(
|
||||
ctx,
|
||||
cl,
|
||||
services,
|
||||
namespace,
|
||||
opts.SendRegistryAuth,
|
||||
opts.ResolveImage,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -265,13 +300,16 @@ func deployCompose(ctx context.Context, cl *dockerClient.Client, opts Deploy, co
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Infof("waiting for %s to deploy... please hold 🤚", appName)
|
||||
|
||||
if err := waitOnServices(ctx, cl, serviceIDs, appName); err != nil {
|
||||
return err
|
||||
waitOpts := WaitOpts{
|
||||
Services: serviceIDs,
|
||||
AppName: appName,
|
||||
ServerName: serverName,
|
||||
Filters: filters,
|
||||
}
|
||||
|
||||
log.Infof("successfully deployed %s", appName)
|
||||
if err := WaitOnServices(ctx, cl, waitOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -343,7 +381,7 @@ func createConfigs(ctx context.Context, cl *dockerClient.Client, configs []swarm
|
||||
}
|
||||
case dockerClient.IsErrNotFound(err):
|
||||
// config does not exist, then we create a new one.
|
||||
log.Infof("creating config %s", configSpec.Name)
|
||||
log.Debugf("creating config %s", configSpec.Name)
|
||||
if _, err := cl.ConfigCreate(ctx, configSpec); err != nil {
|
||||
return errors.Wrapf(err, "failed to create config %s", configSpec.Name)
|
||||
}
|
||||
@ -374,7 +412,7 @@ func createNetworks(ctx context.Context, cl *dockerClient.Client, namespace conv
|
||||
createOpts.Driver = defaultNetworkDriver
|
||||
}
|
||||
|
||||
log.Infof("creating network %s", name)
|
||||
log.Debugf("creating network %s", name)
|
||||
if _, err := cl.NetworkCreate(ctx, name, createOpts); err != nil {
|
||||
return errors.Wrapf(err, "failed to create network %s", name)
|
||||
}
|
||||
@ -388,10 +426,12 @@ func deployServices(
|
||||
services map[string]swarm.ServiceSpec,
|
||||
namespace convert.Namespace,
|
||||
sendAuth bool,
|
||||
resolveImage string) ([]string, error) {
|
||||
resolveImage string) ([]ui.ServiceMeta, error) {
|
||||
var servicesMeta []ui.ServiceMeta
|
||||
|
||||
existingServices, err := GetStackServices(ctx, cl, namespace.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return servicesMeta, err
|
||||
}
|
||||
|
||||
existingServiceMap := make(map[string]swarm.Service)
|
||||
@ -399,8 +439,6 @@ func deployServices(
|
||||
existingServiceMap[service.Spec.Name] = service
|
||||
}
|
||||
|
||||
var serviceIDs []string
|
||||
|
||||
for internalName, serviceSpec := range services {
|
||||
var (
|
||||
name = namespace.Scope(internalName)
|
||||
@ -409,7 +447,7 @@ func deployServices(
|
||||
)
|
||||
|
||||
if service, exists := existingServiceMap[name]; exists {
|
||||
log.Infof("updating %s", name)
|
||||
log.Debugf("updating %s", name)
|
||||
|
||||
updateOpts := types.ServiceUpdateOptions{EncodedRegistryAuth: encodedAuth}
|
||||
|
||||
@ -451,9 +489,12 @@ func deployServices(
|
||||
log.Warn(warning)
|
||||
}
|
||||
|
||||
serviceIDs = append(serviceIDs, service.ID)
|
||||
servicesMeta = append(servicesMeta, ui.ServiceMeta{
|
||||
Name: name,
|
||||
ID: service.ID,
|
||||
})
|
||||
} else {
|
||||
log.Infof("creating %s", name)
|
||||
log.Debugf("creating %s", name)
|
||||
|
||||
createOpts := types.ServiceCreateOptions{EncodedRegistryAuth: encodedAuth}
|
||||
|
||||
@ -467,11 +508,14 @@ func deployServices(
|
||||
return nil, errors.Wrapf(err, "failed to create %s", name)
|
||||
}
|
||||
|
||||
serviceIDs = append(serviceIDs, serviceCreateResponse.ID)
|
||||
servicesMeta = append(servicesMeta, ui.ServiceMeta{
|
||||
Name: name,
|
||||
ID: serviceCreateResponse.ID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return serviceIDs, nil
|
||||
return servicesMeta, nil
|
||||
}
|
||||
|
||||
func getStackNetworks(ctx context.Context, dockerclient client.APIClient, namespace string) ([]networktypes.Inspect, error) {
|
||||
@ -486,67 +530,89 @@ func getStackConfigs(ctx context.Context, dockerclient client.APIClient, namespa
|
||||
return dockerclient.ConfigList(ctx, types.ConfigListOptions{Filters: getStackFilter(namespace)})
|
||||
}
|
||||
|
||||
func waitOnServices(ctx context.Context, cl *dockerClient.Client, serviceIDs []string, appName string) error {
|
||||
var errs []error
|
||||
func timestamp() string {
|
||||
ts := time.Now().UTC().Format(time.RFC3339)
|
||||
return strings.Replace(ts, ":", "", -1) // get rid of offensive colons
|
||||
}
|
||||
|
||||
for _, serviceID := range serviceIDs {
|
||||
if err := WaitOnService(ctx, cl, serviceID, appName); err != nil {
|
||||
errs = append(errs, fmt.Errorf("%s: %w", serviceID, err))
|
||||
}
|
||||
type WaitOpts struct {
|
||||
AppName string
|
||||
Filters filters.Args
|
||||
NoLog bool
|
||||
Quiet bool
|
||||
ServerName string
|
||||
Services []ui.ServiceMeta
|
||||
}
|
||||
|
||||
func WaitOnServices(ctx context.Context, cl *dockerClient.Client, opts WaitOpts) error {
|
||||
timeout := time.Duration(WaitTimeout) * time.Second
|
||||
model := ui.DeployInitialModel(ctx, cl, opts.Services, opts.AppName, timeout, opts.Filters)
|
||||
tui := tea.NewProgram(model)
|
||||
|
||||
if !opts.Quiet {
|
||||
log.Info("polling deployment status")
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
m, err := log.Without(
|
||||
func() (tea.Model, error) {
|
||||
return tui.Run()
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("waitOnServices: error running TUI: %s", err)
|
||||
}
|
||||
|
||||
deployModel := m.(ui.Model)
|
||||
if deployModel.TimedOut || deployModel.Failed || deployModel.Quit {
|
||||
var errs []error
|
||||
|
||||
if deployModel.Failed {
|
||||
errs = append(errs, fmt.Errorf("deploy failed 🛑"))
|
||||
} else if deployModel.TimedOut {
|
||||
errs = append(errs, fmt.Errorf("deploy timed out 🟠"))
|
||||
} else {
|
||||
errs = append(errs, fmt.Errorf("deploy in progress 🟠"))
|
||||
}
|
||||
|
||||
for _, s := range *deployModel.Streams {
|
||||
if s.Err != nil {
|
||||
errs = append(errs, fmt.Errorf("%s: %s", s.Name, s.Err))
|
||||
}
|
||||
}
|
||||
|
||||
if len(*deployModel.Logs) > 0 && !opts.NoLog {
|
||||
logsPath := filepath.Join(
|
||||
config.LOGS_DIR,
|
||||
opts.ServerName,
|
||||
fmt.Sprintf("%s_%s", opts.AppName, timestamp()),
|
||||
)
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(config.LOGS_DIR, opts.ServerName), 0764); err != nil {
|
||||
return fmt.Errorf("waitOnServices: error creating log dir: %s", err)
|
||||
}
|
||||
|
||||
file, err := os.Create(logsPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("waitOnServices: error opening file: %s", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
s := strings.Join(*deployModel.Logs, "\n")
|
||||
if _, err := file.WriteString(s); err != nil {
|
||||
return fmt.Errorf("waitOnServices: writeFile: %s", err)
|
||||
}
|
||||
|
||||
errs = append(errs, fmt.Errorf("logs: %s", logsPath))
|
||||
}
|
||||
|
||||
return stdlibErr.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
func WaitOnService(ctx context.Context, cl *dockerClient.Client, serviceID, appName string) error {
|
||||
errChan := make(chan error, 1)
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
|
||||
sigintChannel := make(chan os.Signal, 1)
|
||||
signal.Notify(sigintChannel, os.Interrupt)
|
||||
defer signal.Stop(sigintChannel)
|
||||
|
||||
go func() {
|
||||
errChan <- progress.ServiceProgress(ctx, cl, serviceID, pipeWriter)
|
||||
}()
|
||||
|
||||
go io.Copy(ioutil.Discard, pipeReader)
|
||||
|
||||
timeout := time.Duration(WaitTimeout) * time.Second
|
||||
|
||||
select {
|
||||
case err := <-errChan:
|
||||
return err
|
||||
case <-sigintChannel:
|
||||
return fmt.Errorf(`
|
||||
Not waiting for %s to deploy. The deployment is ongoing...
|
||||
|
||||
If you want to stop the deployment, try:
|
||||
|
||||
abra app undeploy %s`, appName, appName)
|
||||
case <-time.After(timeout):
|
||||
return fmt.Errorf(`
|
||||
%s has not converged (%s second timeout reached).
|
||||
|
||||
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
|
||||
latency.
|
||||
|
||||
You can track latest deployment status with:
|
||||
|
||||
abra app ps %s
|
||||
|
||||
And inspect the logs with:
|
||||
|
||||
abra app logs %s
|
||||
`, appName, timeout, appName, appName)
|
||||
if !opts.Quiet {
|
||||
log.Info("deploy succeeded 🟢")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Copypasta from https://github.com/docker/cli/blob/master/cli/command/stack/swarm/list.go
|
||||
|
Reference in New Issue
Block a user