package utils import ( "context" "errors" "time" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/volume" "github.com/docker/docker/client" "github.com/docker/docker/errdefs" "github.com/rs/zerolog/log" ) var ( ErrServiceNotFound = errors.New("unable to found the service") ErrServiceCommandFailed = errors.New("command failed on the container") ErrServiceUnhealthy = errors.New("service unhealthy") ) type FnRetry func(ctx context.Context) error func Retry(ctx context.Context, fnRetry FnRetry, waitDuration time.Duration, maxRetry int) (err error) { for i := 0; i < maxRetry; i++ { if err = fnRetry(ctx); err != nil { time.Sleep(waitDuration) continue } return nil } return err } func CreateNetwork(ctx context.Context, cli *client.Client, networkName string) error { networkSpec := types.NetworkCreate{ Driver: "overlay", } _, err := cli.NetworkCreate(ctx, networkName, networkSpec) if err != nil { return err } return nil } func FilterImagesByName(ctx context.Context, cli *client.Client, name string) ([]types.ImageSummary, error) { images, err := cli.ImageList(ctx, types.ImageListOptions{All: true}) if err != nil { return nil, err } var filteredImages []types.ImageSummary for i := range images { for _, tag := range images[i].RepoTags { if tag == name { filteredImages = append(filteredImages, images[i]) break } } } return filteredImages, nil } func CreateService(ctx context.Context, cli *client.Client, spec *swarm.ServiceSpec) error { serviceName := spec.Annotations.Name log.Info().Str("service", serviceName).Msg("creating service...") response, err := cli.ServiceCreate(ctx, *spec, types.ServiceCreateOptions{}) if err != nil { if !errdefs.IsConflict(err) { return err } log.Info().Str("service", serviceName).Msg("service already deployed") return nil } log.Info().Str("service", serviceName).Str("id", response.ID).Msg("service deployed") return nil } func GetServiceByName(ctx context.Context, cli *client.Client, name string) (swarm.Service, error) { servicesFilters := filters.NewArgs() servicesFilters.Add("name", name) options := types.ServiceListOptions{ Filters: servicesFilters, Status: true, } services, err := cli.ServiceList(ctx, options) if err != nil { return swarm.Service{}, err } if len(services) != 1 { return swarm.Service{}, ErrServiceNotFound } return services[0], nil } func CheckServiceHealth(ctx context.Context, cli *client.Client, serviceName string) error { log.Info().Str("service", serviceName).Msg("healthchecking...") service, err := GetServiceByName(ctx, cli, serviceName) if err != nil { return err } if service.ServiceStatus != nil && service.ServiceStatus.RunningTasks != 0 { log.Info().Str("service", serviceName).Msg("service is running") return nil } log.Info().Str("service", serviceName).Msg("service not already started, retrying...") return errors.New("service unhealthy") } func CheckServiceHealthWithRetry(ctx context.Context, cli *client.Client, serviceName string) error { fnRetry := func(ctx context.Context) error { return CheckServiceHealth(ctx, cli, serviceName) } waitDuration := 5 * time.Second maxRetry := 15 return Retry(ctx, fnRetry, waitDuration, maxRetry) } func CheckAndDeleteCompletedService(ctx context.Context, cli *client.Client, name string) error { service, err := GetServiceByName(ctx, cli, name) if err != nil { return err } fnRetry := func(ctx context.Context) error { taskFilters := filters.NewArgs() taskFilters.Add("service", service.ID) tasks, err := cli.TaskList(ctx, types.TaskListOptions{ Filters: taskFilters, }) if err != nil { return err } completeTasks := 0 for idx := range tasks { if tasks[idx].Status.State == swarm.TaskStateComplete { completeTasks++ } } if completeTasks == 0 { return errors.New("no completed tasks") } return nil } maxAttempts := 20 if err := Retry(ctx, fnRetry, time.Second, maxAttempts); err != nil { return err } if err := cli.ServiceRemove(ctx, service.ID); err != nil { return err } return nil } // UpdateServiceByName updates force a service by its name. It will remove and recreate the service. func UpdateServiceByName(ctx context.Context, cli *client.Client, serviceName string) error { srv, err := GetServiceByName(ctx, cli, serviceName) if err != nil { return err } if err := RemoveService(ctx, cli, srv.ID); err != nil { return err } if err := CreateService(ctx, cli, &srv.Spec); err != nil { return err } return nil } func RemoveService(ctx context.Context, cli *client.Client, serviceID string) error { fnRetry := func(ctx context.Context) error { if err := cli.ServiceRemove(ctx, serviceID); err != nil { return err } return nil } maxRetry := 10 if err := Retry(ctx, fnRetry, time.Second, maxRetry); err != nil { return err } return nil } func RemoveImage(ctx context.Context, cli *client.Client, imageID string) error { fnRetry := func(ctx context.Context) error { if _, err := cli.ImageRemove(ctx, imageID, types.ImageRemoveOptions{}); err != nil { if errdefs.IsConflict(err) { log.Warn().Str("image", imageID).Msg("image is using") } return err } return nil } maxRetry := 10 if err := Retry(ctx, fnRetry, time.Second, maxRetry); err != nil { return err } return nil } func CreateVolume(ctx context.Context, cli *client.Client, volumeName string) error { if _, err := cli.VolumeCreate(ctx, volume.CreateOptions{Name: volumeName}); err != nil { if !errdefs.IsConflict(err) { return err } } return nil }