wip: fix: deploy status

toolshed/abra#478
This commit is contained in:
2025-01-18 10:01:59 +01:00
parent d09a19a385
commit 8a19536ace
91 changed files with 8991 additions and 204 deletions

View File

@ -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"
@ -176,7 +176,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{}
@ -190,7 +190,16 @@ 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,
) error {
log.Info("initialising deployment")
if err := validateResolveImageFlag(&opts); err != nil {
return err
}
@ -200,7 +209,7 @@ 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)
}
// validateResolveImageFlag validates the opts.resolveImage command line option
@ -213,7 +222,15 @@ 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,
) error {
namespace := convert.NewNamespace(opts.Namespace)
if opts.Prune {
@ -254,7 +271,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
}
@ -264,14 +288,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 {
if err := WaitOnServices(
ctx,
cl,
serviceIDs,
appName,
serverName,
); err != nil {
return err
}
log.Infof("successfully deployed %s", appName)
return nil
}
@ -342,7 +368,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)
}
@ -373,7 +399,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)
}
@ -387,10 +413,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)
@ -398,8 +426,6 @@ func deployServices(
existingServiceMap[service.Spec.Name] = service
}
var serviceIDs []string
for internalName, serviceSpec := range services {
var (
name = namespace.Scope(internalName)
@ -408,7 +434,7 @@ func deployServices(
)
if service, exists := existingServiceMap[name]; exists {
log.Infof("updating %s", name)
log.Debugf("updating %s", name)
updateOpts := types.ServiceUpdateOptions{EncodedRegistryAuth: encodedAuth}
@ -450,9 +476,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}
@ -466,11 +495,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) ([]types.NetworkResource, error) {
@ -485,69 +517,72 @@ 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))
}
func WaitOnServices(
ctx context.Context,
cl *dockerClient.Client,
services []ui.ServiceMeta,
appName string,
serverName string,
) error {
log.Info("polling deployment status")
timeout := time.Duration(WaitTimeout) * time.Second
model := ui.DeployInitialModel(ctx, cl, services, appName, timeout)
tui := tea.NewProgram(model)
m, err := tui.Run()
if err != nil {
return err
}
if len(errs) > 0 {
deployModel := m.(ui.Model)
logsPath := filepath.Join(
config.LOGS_DIR,
serverName,
fmt.Sprintf("%s_%s", appName, timestamp()),
)
if err := os.MkdirAll(filepath.Join(config.LOGS_DIR, serverName), 0644); err != nil {
// TODO
log.Fatalf("MkdirAll: %s", err)
}
if deployModel.Timeout {
if err := os.WriteFile(logsPath, deployModel.LogsBuffer.Bytes(), 0644); err != nil {
// TODO
log.Fatalf("WriteFile: %s", err)
}
return fmt.Errorf("deployment timed out, logs in %s", logsPath)
}
if deployModel.Failed {
var errs []error
for _, s := range *deployModel.Streams {
if s.Err != nil {
errs = append(errs, fmt.Errorf("%s: %s", s.Name, s.ErrStatus))
}
}
if err := os.WriteFile(logsPath, deployModel.LogsBuffer.Bytes(), 0644); err != nil {
// TODO
log.Fatalf("WriteFile: %s", err)
}
errs = append(errs, fmt.Errorf("deployment failed, logs in %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)
}
}
// 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) {