mirror of
https://github.com/docker/compose.git
synced 2026-02-09 01:59:22 +08:00
move progress UI components into cmd
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
committed by
Guillaume Lours
parent
5ef495c898
commit
aff5c115d6
@@ -35,12 +35,7 @@ var (
|
||||
ErrForbidden = errors.New("forbidden")
|
||||
// ErrUnknown is returned when the error type is unmapped
|
||||
ErrUnknown = errors.New("unknown")
|
||||
// ErrLoginFailed is returned when login failed
|
||||
ErrLoginFailed = errors.New("login failed")
|
||||
// ErrLoginRequired is returned when login is required for a specific action
|
||||
ErrLoginRequired = errors.New("login required")
|
||||
// ErrNotImplemented is returned when a backend doesn't implement
|
||||
// an action
|
||||
// ErrNotImplemented is returned when a backend doesn't implement an action
|
||||
ErrNotImplemented = errors.New("not implemented")
|
||||
// ErrUnsupportedFlag is returned when a backend doesn't support a flag
|
||||
ErrUnsupportedFlag = errors.New("unsupported flag")
|
||||
@@ -48,9 +43,8 @@ var (
|
||||
ErrCanceled = errors.New("canceled")
|
||||
// ErrParsingFailed is returned when a string cannot be parsed
|
||||
ErrParsingFailed = errors.New("parsing failed")
|
||||
// ErrWrongContextType is returned when the caller tries to get a context
|
||||
// with the wrong type
|
||||
ErrWrongContextType = errors.New("wrong context type")
|
||||
// ErrNoResources is returned when operation didn't selected any resource
|
||||
ErrNoResources = errors.New("no resources")
|
||||
)
|
||||
|
||||
// IsNotFoundError returns true if the unwrapped error is ErrNotFound
|
||||
|
||||
103
pkg/api/event.go
Normal file
103
pkg/api/event.go
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// EventStatus indicates the status of an action
|
||||
type EventStatus int
|
||||
|
||||
const (
|
||||
// Working means that the current task is working
|
||||
Working EventStatus = iota
|
||||
// Done means that the current task is done
|
||||
Done
|
||||
// Warning means that the current task has warning
|
||||
Warning
|
||||
// Error means that the current task has errored
|
||||
Error
|
||||
)
|
||||
|
||||
// ResourceCompose is a special resource ID used when event applies to all resources in the application
|
||||
const ResourceCompose = "Compose"
|
||||
|
||||
const (
|
||||
StatusError = "Error"
|
||||
StatusCreating = "Creating"
|
||||
StatusStarting = "Starting"
|
||||
StatusStarted = "Started"
|
||||
StatusWaiting = "Waiting"
|
||||
StatusHealthy = "Healthy"
|
||||
StatusExited = "Exited"
|
||||
StatusRestarting = "Restarting"
|
||||
StatusRestarted = "Restarted"
|
||||
StatusRunning = "Running"
|
||||
StatusCreated = "Created"
|
||||
StatusStopping = "Stopping"
|
||||
StatusStopped = "Stopped"
|
||||
StatusKilling = "Killing"
|
||||
StatusKilled = "Killed"
|
||||
StatusRemoving = "Removing"
|
||||
StatusRemoved = "Removed"
|
||||
StatusBuilding = "Building"
|
||||
StatusBuilt = "Built"
|
||||
StatusPulling = "Pulling"
|
||||
StatusPulled = "Pulled"
|
||||
StatusCommitting = "Committing"
|
||||
StatusCommitted = "Committed"
|
||||
StatusCopying = "Copying"
|
||||
StatusCopied = "Copied"
|
||||
StatusExporting = "Exporting"
|
||||
StatusExported = "Exported"
|
||||
)
|
||||
|
||||
// Resource represents status change and progress for a compose resource.
|
||||
type Resource struct {
|
||||
ID string
|
||||
ParentID string
|
||||
Text string
|
||||
Details string
|
||||
Status EventStatus
|
||||
Current int64
|
||||
Percent int
|
||||
Total int64
|
||||
}
|
||||
|
||||
func (e *Resource) StatusText() string {
|
||||
switch e.Status {
|
||||
case Working:
|
||||
return "Working"
|
||||
case Warning:
|
||||
return "Warning"
|
||||
case Done:
|
||||
return "Done"
|
||||
default:
|
||||
return "Error"
|
||||
}
|
||||
}
|
||||
|
||||
// EventProcessor is notified about Compose operations and tasks
|
||||
type EventProcessor interface {
|
||||
// Start is triggered as a Compose operation is starting with context
|
||||
Start(ctx context.Context, operation string)
|
||||
// On notify about (sub)task and progress processing operation
|
||||
On(events ...Resource)
|
||||
// Done is triggered as a Compose operation completed
|
||||
Done(operation string, success bool)
|
||||
}
|
||||
@@ -26,7 +26,6 @@ import (
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/docker/compose/v2/internal/tracing"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -37,7 +36,7 @@ func (s *composeService) Build(ctx context.Context, project *types.Project, opti
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return progress.Run(ctx, func(ctx context.Context) error {
|
||||
return Run(ctx, func(ctx context.Context) error {
|
||||
return tracing.SpanWrapFunc("project/build", tracing.ProjectOptions(ctx, project),
|
||||
func(ctx context.Context) error {
|
||||
_, err := s.build(ctx, project, options, nil)
|
||||
|
||||
@@ -40,7 +40,6 @@ import (
|
||||
"github.com/docker/cli/cli/command/image/build"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/google/uuid"
|
||||
"github.com/moby/buildkit/client"
|
||||
@@ -118,10 +117,10 @@ type buildStatus struct {
|
||||
func (s *composeService) doBuildBake(ctx context.Context, project *types.Project, serviceToBeBuild types.Services, options api.BuildOptions) (map[string]string, error) { //nolint:gocyclo
|
||||
eg := errgroup.Group{}
|
||||
ch := make(chan *client.SolveStatus)
|
||||
if options.Progress == progress.ModeAuto {
|
||||
displayMode := progressui.DisplayMode(options.Progress)
|
||||
if displayMode == progressui.AutoMode {
|
||||
options.Progress = os.Getenv("BUILDKIT_PROGRESS")
|
||||
}
|
||||
displayMode := progressui.DisplayMode(options.Progress)
|
||||
out := options.Out
|
||||
if out == nil {
|
||||
out = s.stdout()
|
||||
@@ -206,7 +205,7 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
|
||||
}
|
||||
|
||||
image := api.GetImageNameOrDefault(service, project.Name)
|
||||
s.events.On(progress.BuildingEvent(image))
|
||||
s.events.On(buildingEvent(image))
|
||||
|
||||
expectedImages[serviceName] = image
|
||||
|
||||
@@ -408,7 +407,7 @@ func (s *composeService) doBuildBake(ctx context.Context, project *types.Project
|
||||
return nil, fmt.Errorf("build result not found in Bake metadata for service %s", name)
|
||||
}
|
||||
results[image] = built.Digest
|
||||
s.events.On(progress.BuiltEvent(image))
|
||||
s.events.On(builtEvent(image))
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
@@ -554,20 +553,20 @@ func (s composeService) dryRunBake(cfg bakeConfig) map[string]string {
|
||||
bakeResponse[name] = dryRunUUID
|
||||
}
|
||||
for name := range bakeResponse {
|
||||
s.events.On(progress.BuiltEvent(name))
|
||||
s.events.On(builtEvent(name))
|
||||
}
|
||||
return bakeResponse
|
||||
}
|
||||
|
||||
func (s composeService) displayDryRunBuildEvent(name, dryRunUUID, tag string) {
|
||||
s.events.On(progress.Event{
|
||||
s.events.On(api.Resource{
|
||||
ID: name + " ==>",
|
||||
Status: progress.Done,
|
||||
Status: api.Done,
|
||||
Text: fmt.Sprintf("==> writing image %s", dryRunUUID),
|
||||
})
|
||||
s.events.On(progress.Event{
|
||||
s.events.On(api.Resource{
|
||||
ID: name + " ==> ==>",
|
||||
Status: progress.Done,
|
||||
Status: api.Done,
|
||||
Text: fmt.Sprintf(`naming to %s`, tag),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command/image/build"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
progress2 "github.com/docker/compose/v2/pkg/progress"
|
||||
buildtypes "github.com/docker/docker/api/types/build"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
@@ -86,12 +85,12 @@ func (s *composeService) doBuildClassic(ctx context.Context, project *types.Proj
|
||||
}
|
||||
|
||||
image := api.GetImageNameOrDefault(service, project.Name)
|
||||
s.events.On(progress2.BuildingEvent(image))
|
||||
s.events.On(buildingEvent(image))
|
||||
id, err := s.doBuildImage(ctx, project, service, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.events.On(progress2.BuiltEvent(image))
|
||||
s.events.On(builtEvent(image))
|
||||
builtDigests[getServiceIndex(name)] = id
|
||||
|
||||
if options.Push {
|
||||
@@ -258,7 +257,7 @@ func (s *composeService) doBuildImage(ctx context.Context, project *types.Projec
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
s.events.On(progress2.BuildingEvent(imageName))
|
||||
s.events.On(buildingEvent(imageName))
|
||||
response, err := s.apiClient().ImageBuild(ctx, body, buildOpts)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -287,7 +286,7 @@ func (s *composeService) doBuildImage(ctx context.Context, project *types.Projec
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
s.events.On(progress2.BuiltEvent(imageName))
|
||||
s.events.On(builtEvent(imageName))
|
||||
return imageID, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -22,12 +22,11 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
)
|
||||
|
||||
func (s *composeService) Commit(ctx context.Context, projectName string, options api.CommitOptions) error {
|
||||
return progress.Run(ctx, func(ctx context.Context) error {
|
||||
return Run(ctx, func(ctx context.Context) error {
|
||||
return s.commit(ctx, projectName, options)
|
||||
}, "commit", s.events)
|
||||
}
|
||||
@@ -42,17 +41,17 @@ func (s *composeService) commit(ctx context.Context, projectName string, options
|
||||
|
||||
name := getCanonicalContainerName(ctr)
|
||||
|
||||
s.events.On(progress.Event{
|
||||
s.events.On(api.Resource{
|
||||
ID: name,
|
||||
Status: progress.Working,
|
||||
Text: progress.StatusCommitting,
|
||||
Status: api.Working,
|
||||
Text: api.StatusCommitting,
|
||||
})
|
||||
|
||||
if s.dryRun {
|
||||
s.events.On(progress.Event{
|
||||
s.events.On(api.Resource{
|
||||
ID: name,
|
||||
Status: progress.Done,
|
||||
Text: progress.StatusCommitted,
|
||||
Status: api.Done,
|
||||
Text: api.StatusCommitted,
|
||||
})
|
||||
|
||||
return nil
|
||||
@@ -69,10 +68,10 @@ func (s *composeService) commit(ctx context.Context, projectName string, options
|
||||
return err
|
||||
}
|
||||
|
||||
s.events.On(progress.Event{
|
||||
s.events.On(api.Resource{
|
||||
ID: name,
|
||||
Text: fmt.Sprintf("Committed as %s", response.ID),
|
||||
Status: progress.Done,
|
||||
Status: api.Done,
|
||||
})
|
||||
|
||||
return nil
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -32,7 +31,6 @@ import (
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
@@ -45,15 +43,6 @@ import (
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
)
|
||||
|
||||
var stdioToStdout bool
|
||||
|
||||
func init() {
|
||||
out, ok := os.LookupEnv("COMPOSE_STATUS_STDOUT")
|
||||
if ok {
|
||||
stdioToStdout, _ = strconv.ParseBool(out)
|
||||
}
|
||||
}
|
||||
|
||||
type Option func(service *composeService) error
|
||||
|
||||
// NewComposeService creates a Compose service using Docker CLI.
|
||||
@@ -96,7 +85,7 @@ func NewComposeService(dockerCli command.Cli, options ...Option) (api.Compose, e
|
||||
}
|
||||
}
|
||||
if s.events == nil {
|
||||
s.events = progress.NewQuietWriter()
|
||||
s.events = &ignore{}
|
||||
}
|
||||
|
||||
// If custom streams were provided, wrap the Docker CLI to use them
|
||||
@@ -204,7 +193,7 @@ func AlwaysOkPrompt() Prompt {
|
||||
|
||||
// WithEventProcessor configure component to get notified on Compose operation and progress events.
|
||||
// Typically used to configure a progress UI
|
||||
func WithEventProcessor(bus progress.EventProcessor) Option {
|
||||
func WithEventProcessor(bus api.EventProcessor) Option {
|
||||
return func(s *composeService) error {
|
||||
s.events = bus
|
||||
return nil
|
||||
@@ -216,7 +205,7 @@ type composeService struct {
|
||||
// prompt is used to interact with user and confirm actions
|
||||
prompt Prompt
|
||||
// eventBus collects tasks execution events
|
||||
events progress.EventProcessor
|
||||
events api.EventProcessor
|
||||
|
||||
// Optional overrides for specific components (for SDK users)
|
||||
outStream io.Writer
|
||||
@@ -278,13 +267,6 @@ func (s *composeService) stderr() *streams.Out {
|
||||
return s.dockerCli.Err()
|
||||
}
|
||||
|
||||
func (s *composeService) stdinfo() *streams.Out {
|
||||
if stdioToStdout {
|
||||
return s.stdout()
|
||||
}
|
||||
return s.stderr()
|
||||
}
|
||||
|
||||
// readCloserAdapter adapts io.Reader to io.ReadCloser
|
||||
type readCloserAdapter struct {
|
||||
r io.Reader
|
||||
|
||||
@@ -41,7 +41,6 @@ import (
|
||||
|
||||
"github.com/docker/compose/v2/internal/tracing"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -187,7 +186,7 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
|
||||
name := getContainerProgressName(ctr)
|
||||
switch ctr.State {
|
||||
case container.StateRunning:
|
||||
c.compose.events.On(progress.RunningEvent(name))
|
||||
c.compose.events.On(runningEvent(name))
|
||||
case container.StateCreated:
|
||||
case container.StateRestarting:
|
||||
case container.StateExited:
|
||||
@@ -426,16 +425,16 @@ func getContainerProgressName(ctr container.Summary) string {
|
||||
return "Container " + getCanonicalContainerName(ctr)
|
||||
}
|
||||
|
||||
func containerEvents(containers Containers, eventFunc func(string) progress.Event) []progress.Event {
|
||||
events := []progress.Event{}
|
||||
func containerEvents(containers Containers, eventFunc func(string) api.Resource) []api.Resource {
|
||||
events := []api.Resource{}
|
||||
for _, ctr := range containers {
|
||||
events = append(events, eventFunc(getContainerProgressName(ctr)))
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
func containerReasonEvents(containers Containers, eventFunc func(string, string) progress.Event, reason string) []progress.Event {
|
||||
events := []progress.Event{}
|
||||
func containerReasonEvents(containers Containers, eventFunc func(string, string) api.Resource, reason string) []api.Resource {
|
||||
events := []api.Resource{}
|
||||
for _, ctr := range containers {
|
||||
events = append(events, eventFunc(getContainerProgressName(ctr), reason))
|
||||
}
|
||||
@@ -461,7 +460,7 @@ func (s *composeService) waitDependencies(ctx context.Context, project *types.Pr
|
||||
}
|
||||
|
||||
waitingFor := containers.filter(isService(dep), isNotOneOff)
|
||||
s.events.On(containerEvents(waitingFor, progress.Waiting)...)
|
||||
s.events.On(containerEvents(waitingFor, waiting)...)
|
||||
if len(waitingFor) == 0 {
|
||||
if config.Required {
|
||||
return fmt.Errorf("%s is missing dependency %s", dependant, dep)
|
||||
@@ -481,61 +480,61 @@ func (s *composeService) waitDependencies(ctx context.Context, project *types.Pr
|
||||
}
|
||||
switch config.Condition {
|
||||
case ServiceConditionRunningOrHealthy:
|
||||
healthy, err := s.isServiceHealthy(ctx, waitingFor, true)
|
||||
isHealthy, err := s.isServiceHealthy(ctx, waitingFor, true)
|
||||
if err != nil {
|
||||
if !config.Required {
|
||||
s.events.On(containerReasonEvents(waitingFor, progress.SkippedEvent,
|
||||
s.events.On(containerReasonEvents(waitingFor, skippedEvent,
|
||||
fmt.Sprintf("optional dependency %q is not running or is unhealthy", dep))...)
|
||||
logrus.Warnf("optional dependency %q is not running or is unhealthy: %s", dep, err.Error())
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if healthy {
|
||||
s.events.On(containerEvents(waitingFor, progress.Healthy)...)
|
||||
if isHealthy {
|
||||
s.events.On(containerEvents(waitingFor, healthy)...)
|
||||
return nil
|
||||
}
|
||||
case types.ServiceConditionHealthy:
|
||||
healthy, err := s.isServiceHealthy(ctx, waitingFor, false)
|
||||
isHealthy, err := s.isServiceHealthy(ctx, waitingFor, false)
|
||||
if err != nil {
|
||||
if !config.Required {
|
||||
s.events.On(containerReasonEvents(waitingFor, progress.SkippedEvent,
|
||||
s.events.On(containerReasonEvents(waitingFor, skippedEvent,
|
||||
fmt.Sprintf("optional dependency %q failed to start", dep))...)
|
||||
logrus.Warnf("optional dependency %q failed to start: %s", dep, err.Error())
|
||||
return nil
|
||||
}
|
||||
s.events.On(containerEvents(waitingFor, func(s string) progress.Event {
|
||||
return progress.ErrorEventf(s, "dependency %s failed to start", dep)
|
||||
s.events.On(containerEvents(waitingFor, func(s string) api.Resource {
|
||||
return errorEventf(s, "dependency %s failed to start", dep)
|
||||
})...)
|
||||
return fmt.Errorf("dependency failed to start: %w", err)
|
||||
}
|
||||
if healthy {
|
||||
s.events.On(containerEvents(waitingFor, progress.Healthy)...)
|
||||
if isHealthy {
|
||||
s.events.On(containerEvents(waitingFor, healthy)...)
|
||||
return nil
|
||||
}
|
||||
case types.ServiceConditionCompletedSuccessfully:
|
||||
exited, code, err := s.isServiceCompleted(ctx, waitingFor)
|
||||
isExited, code, err := s.isServiceCompleted(ctx, waitingFor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exited {
|
||||
if isExited {
|
||||
if code == 0 {
|
||||
s.events.On(containerEvents(waitingFor, progress.Exited)...)
|
||||
s.events.On(containerEvents(waitingFor, exited)...)
|
||||
return nil
|
||||
}
|
||||
|
||||
messageSuffix := fmt.Sprintf("%q didn't complete successfully: exit %d", dep, code)
|
||||
if !config.Required {
|
||||
// optional -> mark as skipped & don't propagate error
|
||||
s.events.On(containerReasonEvents(waitingFor, progress.SkippedEvent,
|
||||
s.events.On(containerReasonEvents(waitingFor, skippedEvent,
|
||||
fmt.Sprintf("optional dependency %s", messageSuffix))...)
|
||||
logrus.Warnf("optional dependency %s", messageSuffix)
|
||||
return nil
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("service %s", messageSuffix)
|
||||
s.events.On(containerEvents(waitingFor, func(s string) progress.Event {
|
||||
return progress.ErrorEventf(s, "service %s", messageSuffix)
|
||||
s.events.On(containerEvents(waitingFor, func(s string) api.Resource {
|
||||
return errorEventf(s, "service %s", messageSuffix)
|
||||
})...)
|
||||
return errors.New(msg)
|
||||
}
|
||||
@@ -599,19 +598,19 @@ func (s *composeService) createContainer(ctx context.Context, project *types.Pro
|
||||
name string, number int, opts createOptions,
|
||||
) (ctr container.Summary, err error) {
|
||||
eventName := "Container " + name
|
||||
s.events.On(progress.CreatingEvent(eventName))
|
||||
s.events.On(creatingEvent(eventName))
|
||||
ctr, err = s.createMobyContainer(ctx, project, service, name, number, nil, opts)
|
||||
if err != nil {
|
||||
if ctx.Err() == nil {
|
||||
s.events.On(progress.Event{
|
||||
s.events.On(api.Resource{
|
||||
ID: eventName,
|
||||
Status: progress.Error,
|
||||
Status: api.Error,
|
||||
Text: err.Error(),
|
||||
})
|
||||
}
|
||||
return ctr, err
|
||||
}
|
||||
s.events.On(progress.CreatedEvent(eventName))
|
||||
s.events.On(createdEvent(eventName))
|
||||
return ctr, nil
|
||||
}
|
||||
|
||||
@@ -619,12 +618,12 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P
|
||||
replaced container.Summary, inherit bool, timeout *time.Duration,
|
||||
) (created container.Summary, err error) {
|
||||
eventName := getContainerProgressName(replaced)
|
||||
s.events.On(progress.NewEvent(eventName, progress.Working, "Recreate"))
|
||||
s.events.On(newEvent(eventName, api.Working, "Recreate"))
|
||||
defer func() {
|
||||
if err != nil && ctx.Err() == nil {
|
||||
s.events.On(progress.Event{
|
||||
s.events.On(api.Resource{
|
||||
ID: eventName,
|
||||
Status: progress.Error,
|
||||
Status: api.Error,
|
||||
Text: err.Error(),
|
||||
})
|
||||
}
|
||||
@@ -673,7 +672,7 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P
|
||||
return created, err
|
||||
}
|
||||
|
||||
s.events.On(progress.NewEvent(eventName, progress.Done, "Recreated"))
|
||||
s.events.On(newEvent(eventName, api.Done, "Recreated"))
|
||||
return created, err
|
||||
}
|
||||
|
||||
@@ -681,14 +680,14 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P
|
||||
var startMx sync.Mutex
|
||||
|
||||
func (s *composeService) startContainer(ctx context.Context, ctr container.Summary) error {
|
||||
s.events.On(progress.NewEvent(getContainerProgressName(ctr), progress.Working, "Restart"))
|
||||
s.events.On(newEvent(getContainerProgressName(ctr), api.Working, "Restart"))
|
||||
startMx.Lock()
|
||||
defer startMx.Unlock()
|
||||
err := s.apiClient().ContainerStart(ctx, ctr.ID, container.StartOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.events.On(progress.NewEvent(getContainerProgressName(ctr), progress.Done, "Restarted"))
|
||||
s.events.On(newEvent(getContainerProgressName(ctr), api.Done, "Restarted"))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -719,9 +718,9 @@ func (s *composeService) createMobyContainer(ctx context.Context, project *types
|
||||
return created, err
|
||||
}
|
||||
for _, warning := range response.Warnings {
|
||||
s.events.On(progress.Event{
|
||||
s.events.On(api.Resource{
|
||||
ID: service.Name,
|
||||
Status: progress.Warning,
|
||||
Status: api.Warning,
|
||||
Text: warning,
|
||||
})
|
||||
}
|
||||
@@ -906,7 +905,7 @@ func (s *composeService) startService(ctx context.Context,
|
||||
}
|
||||
|
||||
eventName := getContainerProgressName(ctr)
|
||||
s.events.On(progress.StartingEvent(eventName))
|
||||
s.events.On(startingEvent(eventName))
|
||||
err = s.apiClient().ContainerStart(ctx, ctr.ID, container.StartOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -919,7 +918,7 @@ func (s *composeService) startService(ctx context.Context,
|
||||
}
|
||||
}
|
||||
|
||||
s.events.On(progress.StartedEvent(eventName))
|
||||
s.events.On(startedEvent(eventName))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
@@ -43,7 +42,7 @@ const (
|
||||
)
|
||||
|
||||
func (s *composeService) Copy(ctx context.Context, projectName string, options api.CopyOptions) error {
|
||||
return progress.Run(ctx, func(ctx context.Context) error {
|
||||
return Run(ctx, func(ctx context.Context) error {
|
||||
return s.copy(ctx, projectName, options)
|
||||
}, "copy", s.events)
|
||||
}
|
||||
@@ -90,20 +89,20 @@ func (s *composeService) copy(ctx context.Context, projectName string, options a
|
||||
} else {
|
||||
msg = fmt.Sprintf("%s to %s:%s", srcPath, name, dstPath)
|
||||
}
|
||||
s.events.On(progress.Event{
|
||||
s.events.On(api.Resource{
|
||||
ID: name,
|
||||
Text: progress.StatusCopying,
|
||||
Text: api.StatusCopying,
|
||||
Details: msg,
|
||||
Status: progress.Working,
|
||||
Status: api.Working,
|
||||
})
|
||||
if err := copyFunc(ctx, ctr.ID, srcPath, dstPath, options); err != nil {
|
||||
return err
|
||||
}
|
||||
s.events.On(progress.Event{
|
||||
s.events.On(api.Resource{
|
||||
ID: name,
|
||||
Text: progress.StatusCopied,
|
||||
Text: api.StatusCopied,
|
||||
Details: msg,
|
||||
Status: progress.Done,
|
||||
Status: api.Done,
|
||||
})
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -43,7 +43,6 @@ import (
|
||||
cdi "tags.cncf.io/container-device-interface/pkg/parser"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
)
|
||||
|
||||
type createOptions struct {
|
||||
@@ -61,7 +60,7 @@ type createConfigs struct {
|
||||
}
|
||||
|
||||
func (s *composeService) Create(ctx context.Context, project *types.Project, createOpts api.CreateOptions) error {
|
||||
return progress.Run(ctx, func(ctx context.Context) error {
|
||||
return Run(ctx, func(ctx context.Context) error {
|
||||
return s.create(ctx, project, createOpts)
|
||||
}, "create", s.events)
|
||||
}
|
||||
@@ -1394,14 +1393,14 @@ func (s *composeService) resolveOrCreateNetwork(ctx context.Context, project *ty
|
||||
}
|
||||
|
||||
networkEventName := fmt.Sprintf("Network %s", n.Name)
|
||||
s.events.On(progress.CreatingEvent(networkEventName))
|
||||
s.events.On(creatingEvent(networkEventName))
|
||||
|
||||
resp, err := s.apiClient().NetworkCreate(ctx, n.Name, createOpts)
|
||||
if err != nil {
|
||||
s.events.On(progress.ErrorEvent(networkEventName, err.Error()))
|
||||
s.events.On(errorEvent(networkEventName, err.Error()))
|
||||
return "", fmt.Errorf("failed to create network %s: %w", n.Name, err)
|
||||
}
|
||||
s.events.On(progress.CreatedEvent(networkEventName))
|
||||
s.events.On(createdEvent(networkEventName))
|
||||
|
||||
err = s.connectNetwork(ctx, n.Name, dangledContainers, nil)
|
||||
if err != nil {
|
||||
@@ -1443,7 +1442,7 @@ func (s *composeService) removeDivergedNetwork(ctx context.Context, project *typ
|
||||
|
||||
err = s.apiClient().NetworkRemove(ctx, n.Name)
|
||||
eventName := fmt.Sprintf("Network %s", n.Name)
|
||||
s.events.On(progress.RemovedEvent(eventName))
|
||||
s.events.On(removedEvent(eventName))
|
||||
return containers, err
|
||||
}
|
||||
|
||||
@@ -1619,7 +1618,7 @@ func (s *composeService) removeDivergedVolume(ctx context.Context, name string,
|
||||
|
||||
func (s *composeService) createVolume(ctx context.Context, volume types.VolumeConfig) error {
|
||||
eventName := fmt.Sprintf("Volume %s", volume.Name)
|
||||
s.events.On(progress.CreatingEvent(eventName))
|
||||
s.events.On(creatingEvent(eventName))
|
||||
hash, err := VolumeHash(volume)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1632,9 +1631,9 @@ func (s *composeService) createVolume(ctx context.Context, volume types.VolumeCo
|
||||
DriverOpts: volume.DriverOpts,
|
||||
})
|
||||
if err != nil {
|
||||
s.events.On(progress.ErrorEvent(eventName, err.Error()))
|
||||
s.events.On(errorEvent(eventName, err.Error()))
|
||||
return err
|
||||
}
|
||||
s.events.On(progress.CreatedEvent(eventName))
|
||||
s.events.On(createdEvent(eventName))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ import (
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
containerType "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
@@ -38,7 +37,7 @@ import (
|
||||
type downOp func() error
|
||||
|
||||
func (s *composeService) Down(ctx context.Context, projectName string, options api.DownOptions) error {
|
||||
return progress.Run(ctx, func(ctx context.Context) error {
|
||||
return Run(ctx, func(ctx context.Context) error {
|
||||
return s.down(ctx, strings.ToLower(projectName), options)
|
||||
}, "down", s.events)
|
||||
}
|
||||
@@ -210,7 +209,7 @@ func (s *composeService) removeNetwork(ctx context.Context, composeNetworkName s
|
||||
}
|
||||
|
||||
eventName := fmt.Sprintf("Network %s", name)
|
||||
s.events.On(progress.RemovingEvent(eventName))
|
||||
s.events.On(removingEvent(eventName))
|
||||
|
||||
var found int
|
||||
for _, net := range networks {
|
||||
@@ -219,14 +218,14 @@ func (s *composeService) removeNetwork(ctx context.Context, composeNetworkName s
|
||||
}
|
||||
nw, err := s.apiClient().NetworkInspect(ctx, net.ID, network.InspectOptions{})
|
||||
if errdefs.IsNotFound(err) {
|
||||
s.events.On(progress.NewEvent(eventName, progress.Warning, "No resource found to remove"))
|
||||
s.events.On(newEvent(eventName, api.Warning, "No resource found to remove"))
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(nw.Containers) > 0 {
|
||||
s.events.On(progress.NewEvent(eventName, progress.Warning, "Resource is still in use"))
|
||||
s.events.On(newEvent(eventName, api.Warning, "Resource is still in use"))
|
||||
found++
|
||||
continue
|
||||
}
|
||||
@@ -235,10 +234,10 @@ func (s *composeService) removeNetwork(ctx context.Context, composeNetworkName s
|
||||
if errdefs.IsNotFound(err) {
|
||||
continue
|
||||
}
|
||||
s.events.On(progress.ErrorEvent(eventName, err.Error()))
|
||||
s.events.On(errorEvent(eventName, err.Error()))
|
||||
return fmt.Errorf("failed to remove network %s: %w", name, err)
|
||||
}
|
||||
s.events.On(progress.RemovedEvent(eventName))
|
||||
s.events.On(removedEvent(eventName))
|
||||
found++
|
||||
}
|
||||
|
||||
@@ -246,7 +245,7 @@ func (s *composeService) removeNetwork(ctx context.Context, composeNetworkName s
|
||||
// in practice, it's extremely unlikely for this to ever occur, as it'd
|
||||
// mean the network was present when we queried at the start of this
|
||||
// method but was then deleted by something else in the interim
|
||||
s.events.On(progress.NewEvent(eventName, progress.Warning, "No resource found to remove"))
|
||||
s.events.On(newEvent(eventName, api.Warning, "No resource found to remove"))
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
@@ -254,18 +253,18 @@ func (s *composeService) removeNetwork(ctx context.Context, composeNetworkName s
|
||||
|
||||
func (s *composeService) removeImage(ctx context.Context, image string) error {
|
||||
id := fmt.Sprintf("Image %s", image)
|
||||
s.events.On(progress.NewEvent(id, progress.Working, "Removing"))
|
||||
s.events.On(newEvent(id, api.Working, "Removing"))
|
||||
_, err := s.apiClient().ImageRemove(ctx, image, imageapi.RemoveOptions{})
|
||||
if err == nil {
|
||||
s.events.On(progress.NewEvent(id, progress.Done, "Removed"))
|
||||
s.events.On(newEvent(id, api.Done, "Removed"))
|
||||
return nil
|
||||
}
|
||||
if errdefs.IsConflict(err) {
|
||||
s.events.On(progress.NewEvent(id, progress.Warning, "Resource is still in use"))
|
||||
s.events.On(newEvent(id, api.Warning, "Resource is still in use"))
|
||||
return nil
|
||||
}
|
||||
if errdefs.IsNotFound(err) {
|
||||
s.events.On(progress.NewEvent(id, progress.Done, "Warning: No resource found to remove"))
|
||||
s.events.On(newEvent(id, api.Done, "Warning: No resource found to remove"))
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
@@ -280,18 +279,18 @@ func (s *composeService) removeVolume(ctx context.Context, id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
s.events.On(progress.NewEvent(resource, progress.Working, "Removing"))
|
||||
s.events.On(newEvent(resource, api.Working, "Removing"))
|
||||
err = s.apiClient().VolumeRemove(ctx, id, true)
|
||||
if err == nil {
|
||||
s.events.On(progress.NewEvent(resource, progress.Done, "Removed"))
|
||||
s.events.On(newEvent(resource, api.Done, "Removed"))
|
||||
return nil
|
||||
}
|
||||
if errdefs.IsConflict(err) {
|
||||
s.events.On(progress.NewEvent(resource, progress.Warning, "Resource is still in use"))
|
||||
s.events.On(newEvent(resource, api.Warning, "Resource is still in use"))
|
||||
return nil
|
||||
}
|
||||
if errdefs.IsNotFound(err) {
|
||||
s.events.On(progress.NewEvent(resource, progress.Done, "Warning: No resource found to remove"))
|
||||
s.events.On(newEvent(resource, api.Done, "Warning: No resource found to remove"))
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
@@ -299,7 +298,7 @@ func (s *composeService) removeVolume(ctx context.Context, id string) error {
|
||||
|
||||
func (s *composeService) stopContainer(ctx context.Context, service *types.ServiceConfig, ctr containerType.Summary, timeout *time.Duration, listener api.ContainerEventListener) error {
|
||||
eventName := getContainerProgressName(ctr)
|
||||
s.events.On(progress.StoppingEvent(eventName))
|
||||
s.events.On(stoppingEvent(eventName))
|
||||
|
||||
if service != nil {
|
||||
for _, hook := range service.PreStop {
|
||||
@@ -317,10 +316,10 @@ func (s *composeService) stopContainer(ctx context.Context, service *types.Servi
|
||||
timeoutInSecond := utils.DurationSecondToInt(timeout)
|
||||
err := s.apiClient().ContainerStop(ctx, ctr.ID, containerType.StopOptions{Timeout: timeoutInSecond})
|
||||
if err != nil {
|
||||
s.events.On(progress.ErrorEvent(eventName, "Error while Stopping"))
|
||||
s.events.On(errorEvent(eventName, "Error while Stopping"))
|
||||
return err
|
||||
}
|
||||
s.events.On(progress.StoppedEvent(eventName))
|
||||
s.events.On(stoppedEvent(eventName))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -348,22 +347,22 @@ func (s *composeService) stopAndRemoveContainer(ctx context.Context, ctr contain
|
||||
eventName := getContainerProgressName(ctr)
|
||||
err := s.stopContainer(ctx, service, ctr, timeout, nil)
|
||||
if errdefs.IsNotFound(err) {
|
||||
s.events.On(progress.RemovedEvent(eventName))
|
||||
s.events.On(removedEvent(eventName))
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.events.On(progress.RemovingEvent(eventName))
|
||||
s.events.On(removingEvent(eventName))
|
||||
err = s.apiClient().ContainerRemove(ctx, ctr.ID, containerType.RemoveOptions{
|
||||
Force: true,
|
||||
RemoveVolumes: volumes,
|
||||
})
|
||||
if err != nil && !errdefs.IsNotFound(err) && !errdefs.IsConflict(err) {
|
||||
s.events.On(progress.ErrorEvent(eventName, "Error while Removing"))
|
||||
s.events.On(errorEvent(eventName, "Error while Removing"))
|
||||
return err
|
||||
}
|
||||
s.events.On(progress.RemovedEvent(eventName))
|
||||
s.events.On(removedEvent(eventName))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -24,12 +24,11 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/moby/sys/atomicwriter"
|
||||
)
|
||||
|
||||
func (s *composeService) Export(ctx context.Context, projectName string, options api.ExportOptions) error {
|
||||
return progress.Run(ctx, func(ctx context.Context) error {
|
||||
return Run(ctx, func(ctx context.Context) error {
|
||||
return s.export(ctx, projectName, options)
|
||||
}, "export", s.events)
|
||||
}
|
||||
@@ -51,10 +50,10 @@ func (s *composeService) export(ctx context.Context, projectName string, options
|
||||
}
|
||||
|
||||
name := getCanonicalContainerName(container)
|
||||
s.events.On(progress.Event{
|
||||
s.events.On(api.Resource{
|
||||
ID: name,
|
||||
Text: progress.StatusExporting,
|
||||
Status: progress.Working,
|
||||
Text: api.StatusExporting,
|
||||
Status: api.Working,
|
||||
})
|
||||
|
||||
responseBody, err := s.apiClient().ContainerExport(ctx, container.ID)
|
||||
@@ -64,7 +63,7 @@ func (s *composeService) export(ctx context.Context, projectName string, options
|
||||
|
||||
defer func() {
|
||||
if err := responseBody.Close(); err != nil {
|
||||
s.events.On(progress.ErrorEventf(name, "Failed to close response body: %s", err.Error()))
|
||||
s.events.On(errorEventf(name, "Failed to close response body: %s", err.Error()))
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -84,10 +83,10 @@ func (s *composeService) export(ctx context.Context, projectName string, options
|
||||
}
|
||||
}
|
||||
|
||||
s.events.On(progress.Event{
|
||||
s.events.On(api.Resource{
|
||||
ID: name,
|
||||
Text: progress.StatusExported,
|
||||
Status: progress.Done,
|
||||
Text: api.StatusExported,
|
||||
Status: api.Done,
|
||||
})
|
||||
|
||||
return nil
|
||||
|
||||
@@ -18,18 +18,16 @@ package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
)
|
||||
|
||||
func (s *composeService) Kill(ctx context.Context, projectName string, options api.KillOptions) error {
|
||||
return progress.Run(ctx, func(ctx context.Context) error {
|
||||
return Run(ctx, func(ctx context.Context) error {
|
||||
return s.kill(ctx, strings.ToLower(projectName), options)
|
||||
}, "kill", s.events)
|
||||
}
|
||||
@@ -55,21 +53,20 @@ func (s *composeService) kill(ctx context.Context, projectName string, options a
|
||||
containers = containers.filter(isService(project.ServiceNames()...))
|
||||
}
|
||||
if len(containers) == 0 {
|
||||
_, _ = fmt.Fprintf(s.stdinfo(), "no container to kill")
|
||||
return nil
|
||||
return api.ErrNoResources
|
||||
}
|
||||
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
containers.forEach(func(ctr container.Summary) {
|
||||
eg.Go(func() error {
|
||||
eventName := getContainerProgressName(ctr)
|
||||
s.events.On(progress.KillingEvent(eventName))
|
||||
s.events.On(killingEvent(eventName))
|
||||
err := s.apiClient().ContainerKill(ctx, ctr.ID, options.Signal)
|
||||
if err != nil {
|
||||
s.events.On(progress.ErrorEvent(eventName, "Error while Killing"))
|
||||
s.events.On(errorEvent(eventName, "Error while Killing"))
|
||||
return err
|
||||
}
|
||||
s.events.On(progress.KilledEvent(eventName))
|
||||
s.events.On(killedEvent(eventName))
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
@@ -29,7 +29,7 @@ import (
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
@@ -101,10 +101,10 @@ func (m *modelAPI) Close() {
|
||||
m.cleanup()
|
||||
}
|
||||
|
||||
func (m *modelAPI) PullModel(ctx context.Context, model types.ModelConfig, quietPull bool, events progress.EventProcessor) error {
|
||||
events.On(progress.Event{
|
||||
func (m *modelAPI) PullModel(ctx context.Context, model types.ModelConfig, quietPull bool, events api.EventProcessor) error {
|
||||
events.On(api.Resource{
|
||||
ID: model.Name,
|
||||
Status: progress.Working,
|
||||
Status: api.Working,
|
||||
Text: "Pulling",
|
||||
})
|
||||
|
||||
@@ -131,30 +131,30 @@ func (m *modelAPI) PullModel(ctx context.Context, model types.ModelConfig, quiet
|
||||
}
|
||||
|
||||
if !quietPull {
|
||||
events.On(progress.Event{
|
||||
events.On(api.Resource{
|
||||
ID: model.Name,
|
||||
Status: progress.Working,
|
||||
Text: progress.StatusPulling,
|
||||
Status: api.Working,
|
||||
Text: api.StatusPulling,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
events.On(progress.ErrorEvent(model.Name, err.Error()))
|
||||
events.On(errorEvent(model.Name, err.Error()))
|
||||
}
|
||||
events.On(progress.Event{
|
||||
events.On(api.Resource{
|
||||
ID: model.Name,
|
||||
Status: progress.Working,
|
||||
Text: progress.StatusPulled,
|
||||
Status: api.Working,
|
||||
Text: api.StatusPulled,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *modelAPI) ConfigureModel(ctx context.Context, config types.ModelConfig, events progress.EventProcessor) error {
|
||||
events.On(progress.Event{
|
||||
func (m *modelAPI) ConfigureModel(ctx context.Context, config types.ModelConfig, events api.EventProcessor) error {
|
||||
events.On(api.Resource{
|
||||
ID: config.Name,
|
||||
Status: progress.Working,
|
||||
Status: api.Working,
|
||||
Text: "Configuring",
|
||||
})
|
||||
// configure [--context-size=<n>] MODEL [-- <runtime-flags...>]
|
||||
|
||||
@@ -24,11 +24,10 @@ import (
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
)
|
||||
|
||||
func (s *composeService) Pause(ctx context.Context, projectName string, options api.PauseOptions) error {
|
||||
return progress.Run(ctx, func(ctx context.Context) error {
|
||||
return Run(ctx, func(ctx context.Context) error {
|
||||
return s.pause(ctx, strings.ToLower(projectName), options)
|
||||
}, "pause", s.events)
|
||||
}
|
||||
@@ -49,7 +48,7 @@ func (s *composeService) pause(ctx context.Context, projectName string, options
|
||||
err := s.apiClient().ContainerPause(ctx, container.ID)
|
||||
if err == nil {
|
||||
eventName := getContainerProgressName(container)
|
||||
s.events.On(progress.NewEvent(eventName, progress.Done, "Paused"))
|
||||
s.events.On(newEvent(eventName, api.Done, "Paused"))
|
||||
}
|
||||
return err
|
||||
})
|
||||
@@ -58,7 +57,7 @@ func (s *composeService) pause(ctx context.Context, projectName string, options
|
||||
}
|
||||
|
||||
func (s *composeService) UnPause(ctx context.Context, projectName string, options api.PauseOptions) error {
|
||||
return progress.Run(ctx, func(ctx context.Context) error {
|
||||
return Run(ctx, func(ctx context.Context) error {
|
||||
return s.unPause(ctx, strings.ToLower(projectName), options)
|
||||
}, "unpause", s.events)
|
||||
}
|
||||
@@ -79,7 +78,7 @@ func (s *composeService) unPause(ctx context.Context, projectName string, option
|
||||
err = s.apiClient().ContainerUnpause(ctx, ctr.ID)
|
||||
if err == nil {
|
||||
eventName := getContainerProgressName(ctr)
|
||||
s.events.On(progress.NewEvent(eventName, progress.Done, "Unpaused"))
|
||||
s.events.On(newEvent(eventName, api.Done, "Unpaused"))
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
||||
@@ -33,7 +33,7 @@ import (
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -89,10 +89,10 @@ func (s *composeService) executePlugin(cmd *exec.Cmd, command string, service ty
|
||||
var action string
|
||||
switch command {
|
||||
case "up":
|
||||
s.events.On(progress.CreatingEvent(service.Name))
|
||||
s.events.On(creatingEvent(service.Name))
|
||||
action = "create"
|
||||
case "down":
|
||||
s.events.On(progress.RemovingEvent(service.Name))
|
||||
s.events.On(removingEvent(service.Name))
|
||||
action = "remove"
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported plugin command: %s", command)
|
||||
@@ -124,10 +124,10 @@ func (s *composeService) executePlugin(cmd *exec.Cmd, command string, service ty
|
||||
}
|
||||
switch msg.Type {
|
||||
case ErrorType:
|
||||
s.events.On(progress.NewEvent(service.Name, progress.Error, msg.Message))
|
||||
s.events.On(newEvent(service.Name, api.Error, msg.Message))
|
||||
return nil, errors.New(msg.Message)
|
||||
case InfoType:
|
||||
s.events.On(progress.NewEvent(service.Name, progress.Working, msg.Message))
|
||||
s.events.On(newEvent(service.Name, api.Working, msg.Message))
|
||||
case SetEnvType:
|
||||
key, val, found := strings.Cut(msg.Message, "=")
|
||||
if !found {
|
||||
@@ -143,14 +143,14 @@ func (s *composeService) executePlugin(cmd *exec.Cmd, command string, service ty
|
||||
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
s.events.On(progress.ErrorEvent(service.Name, err.Error()))
|
||||
s.events.On(errorEvent(service.Name, err.Error()))
|
||||
return nil, fmt.Errorf("failed to %s service provider: %s", action, err.Error())
|
||||
}
|
||||
switch command {
|
||||
case "up":
|
||||
s.events.On(progress.CreatedEvent(service.Name))
|
||||
s.events.On(createdEvent(service.Name))
|
||||
case "down":
|
||||
s.events.On(progress.RemovedEvent(service.Name))
|
||||
s.events.On(removedEvent(service.Name))
|
||||
}
|
||||
return variables, nil
|
||||
}
|
||||
|
||||
176
pkg/compose/progress.go
Normal file
176
pkg/compose/progress.go
Normal file
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
)
|
||||
|
||||
type progressFunc func(context.Context) error
|
||||
|
||||
func Run(ctx context.Context, pf progressFunc, operation string, bus api.EventProcessor) error {
|
||||
bus.Start(ctx, operation)
|
||||
err := pf(ctx)
|
||||
bus.Done(operation, err != nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// errorEvent creates a new Error Resource with message
|
||||
func errorEvent(id string, msg string) api.Resource {
|
||||
return api.Resource{
|
||||
ID: id,
|
||||
Status: api.Error,
|
||||
Text: api.StatusError,
|
||||
Details: msg,
|
||||
}
|
||||
}
|
||||
|
||||
// errorEventf creates a new Error Resource with format message
|
||||
func errorEventf(id string, msg string, args ...any) api.Resource {
|
||||
return errorEvent(id, fmt.Sprintf(msg, args...))
|
||||
}
|
||||
|
||||
// creatingEvent creates a new Create in progress Resource
|
||||
func creatingEvent(id string) api.Resource {
|
||||
return newEvent(id, api.Working, api.StatusCreating)
|
||||
}
|
||||
|
||||
// startingEvent creates a new Starting in progress Resource
|
||||
func startingEvent(id string) api.Resource {
|
||||
return newEvent(id, api.Working, api.StatusStarting)
|
||||
}
|
||||
|
||||
// startedEvent creates a new Started in progress Resource
|
||||
func startedEvent(id string) api.Resource {
|
||||
return newEvent(id, api.Done, api.StatusStarted)
|
||||
}
|
||||
|
||||
// waiting creates a new waiting event
|
||||
func waiting(id string) api.Resource {
|
||||
return newEvent(id, api.Working, api.StatusWaiting)
|
||||
}
|
||||
|
||||
// healthy creates a new healthy event
|
||||
func healthy(id string) api.Resource {
|
||||
return newEvent(id, api.Done, api.StatusHealthy)
|
||||
}
|
||||
|
||||
// exited creates a new exited event
|
||||
func exited(id string) api.Resource {
|
||||
return newEvent(id, api.Done, api.StatusExited)
|
||||
}
|
||||
|
||||
// restartingEvent creates a new Restarting in progress Resource
|
||||
func restartingEvent(id string) api.Resource {
|
||||
return newEvent(id, api.Working, api.StatusRestarting)
|
||||
}
|
||||
|
||||
// runningEvent creates a new Running in progress Resource
|
||||
func runningEvent(id string) api.Resource {
|
||||
return newEvent(id, api.Done, api.StatusRunning)
|
||||
}
|
||||
|
||||
// createdEvent creates a new Created (done) Resource
|
||||
func createdEvent(id string) api.Resource {
|
||||
return newEvent(id, api.Done, api.StatusCreated)
|
||||
}
|
||||
|
||||
// stoppingEvent creates a new Stopping in progress Resource
|
||||
func stoppingEvent(id string) api.Resource {
|
||||
return newEvent(id, api.Working, api.StatusStopping)
|
||||
}
|
||||
|
||||
// stoppedEvent creates a new Stopping in progress Resource
|
||||
func stoppedEvent(id string) api.Resource {
|
||||
return newEvent(id, api.Done, api.StatusStopped)
|
||||
}
|
||||
|
||||
// killingEvent creates a new Killing in progress Resource
|
||||
func killingEvent(id string) api.Resource {
|
||||
return newEvent(id, api.Working, api.StatusKilling)
|
||||
}
|
||||
|
||||
// killedEvent creates a new Killed in progress Resource
|
||||
func killedEvent(id string) api.Resource {
|
||||
return newEvent(id, api.Done, api.StatusKilled)
|
||||
}
|
||||
|
||||
// removingEvent creates a new Removing in progress Resource
|
||||
func removingEvent(id string) api.Resource {
|
||||
return newEvent(id, api.Working, api.StatusRemoving)
|
||||
}
|
||||
|
||||
// removedEvent creates a new removed (done) Resource
|
||||
func removedEvent(id string) api.Resource {
|
||||
return newEvent(id, api.Done, api.StatusRemoved)
|
||||
}
|
||||
|
||||
// buildingEvent creates a new Building in progress Resource
|
||||
func buildingEvent(id string) api.Resource {
|
||||
return newEvent("Image "+id, api.Working, api.StatusBuilding)
|
||||
}
|
||||
|
||||
// builtEvent creates a new built (done) Resource
|
||||
func builtEvent(id string) api.Resource {
|
||||
return newEvent("Image "+id, api.Done, api.StatusBuilt)
|
||||
}
|
||||
|
||||
// pullingEvent creates a new pulling (in progress) Resource
|
||||
func pullingEvent(id string) api.Resource {
|
||||
return newEvent("Image "+id, api.Working, api.StatusPulling)
|
||||
}
|
||||
|
||||
// pulledEvent creates a new pulled (done) Resource
|
||||
func pulledEvent(id string) api.Resource {
|
||||
return newEvent("Image "+id, api.Done, api.StatusPulled)
|
||||
}
|
||||
|
||||
// skippedEvent creates a new Skipped Resource
|
||||
func skippedEvent(id string, reason string) api.Resource {
|
||||
return api.Resource{
|
||||
ID: id,
|
||||
Status: api.Warning,
|
||||
Text: "Skipped: " + reason,
|
||||
}
|
||||
}
|
||||
|
||||
// newEvent new event
|
||||
func newEvent(id string, status api.EventStatus, text string, reason ...string) api.Resource {
|
||||
r := api.Resource{
|
||||
ID: id,
|
||||
Status: status,
|
||||
Text: text,
|
||||
}
|
||||
if len(reason) > 0 {
|
||||
r.Details = reason[0]
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
type ignore struct{}
|
||||
|
||||
func (q *ignore) Start(_ context.Context, _ string) {
|
||||
}
|
||||
|
||||
func (q *ignore) Done(_ string, _ bool) {
|
||||
}
|
||||
|
||||
func (q *ignore) On(_ ...api.Resource) {
|
||||
}
|
||||
@@ -35,7 +35,6 @@ import (
|
||||
"github.com/docker/compose/v2/internal/oci"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/compose/transform"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/opencontainers/image-spec/specs-go"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
@@ -43,7 +42,7 @@ import (
|
||||
)
|
||||
|
||||
func (s *composeService) Publish(ctx context.Context, project *types.Project, repository string, options api.PublishOptions) error {
|
||||
return progress.Run(ctx, func(ctx context.Context) error {
|
||||
return Run(ctx, func(ctx context.Context) error {
|
||||
return s.publish(ctx, project, repository, options)
|
||||
}, "publish", s.events)
|
||||
}
|
||||
@@ -71,10 +70,10 @@ func (s *composeService) publish(ctx context.Context, project *types.Project, re
|
||||
return err
|
||||
}
|
||||
|
||||
s.events.On(progress.Event{
|
||||
s.events.On(api.Resource{
|
||||
ID: repository,
|
||||
Text: "publishing",
|
||||
Status: progress.Working,
|
||||
Status: api.Working,
|
||||
})
|
||||
if logrus.IsLevelEnabled(logrus.DebugLevel) {
|
||||
logrus.Debug("publishing layers")
|
||||
@@ -98,10 +97,10 @@ func (s *composeService) publish(ctx context.Context, project *types.Project, re
|
||||
|
||||
descriptor, err := oci.PushManifest(ctx, resolver, named, layers, options.OCIVersion)
|
||||
if err != nil {
|
||||
s.events.On(progress.Event{
|
||||
s.events.On(api.Resource{
|
||||
ID: repository,
|
||||
Text: "publishing",
|
||||
Status: progress.Error,
|
||||
Status: api.Error,
|
||||
})
|
||||
return err
|
||||
}
|
||||
@@ -150,10 +149,10 @@ func (s *composeService) publish(ctx context.Context, project *types.Project, re
|
||||
}
|
||||
}
|
||||
}
|
||||
s.events.On(progress.Event{
|
||||
s.events.On(api.Resource{
|
||||
ID: repository,
|
||||
Text: "published",
|
||||
Status: progress.Done,
|
||||
Status: api.Done,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -40,11 +40,10 @@ import (
|
||||
|
||||
"github.com/docker/compose/v2/internal/registry"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
)
|
||||
|
||||
func (s *composeService) Pull(ctx context.Context, project *types.Project, options api.PullOptions) error {
|
||||
return progress.Run(ctx, func(ctx context.Context) error {
|
||||
return Run(ctx, func(ctx context.Context) error {
|
||||
return s.pull(ctx, project, options)
|
||||
}, "pull", s.events)
|
||||
}
|
||||
@@ -67,9 +66,9 @@ func (s *composeService) pull(ctx context.Context, project *types.Project, opts
|
||||
i := 0
|
||||
for name, service := range project.Services {
|
||||
if service.Image == "" {
|
||||
s.events.On(progress.Event{
|
||||
s.events.On(api.Resource{
|
||||
ID: name,
|
||||
Status: progress.Done,
|
||||
Status: api.Done,
|
||||
Text: "Skipped",
|
||||
Details: "No image to be pulled",
|
||||
})
|
||||
@@ -78,17 +77,17 @@ func (s *composeService) pull(ctx context.Context, project *types.Project, opts
|
||||
|
||||
switch service.PullPolicy {
|
||||
case types.PullPolicyNever, types.PullPolicyBuild:
|
||||
s.events.On(progress.Event{
|
||||
s.events.On(api.Resource{
|
||||
ID: "Image " + service.Image,
|
||||
Status: progress.Done,
|
||||
Status: api.Done,
|
||||
Text: "Skipped",
|
||||
})
|
||||
continue
|
||||
case types.PullPolicyMissing, types.PullPolicyIfNotPresent:
|
||||
if imageAlreadyPresent(service.Image, images) {
|
||||
s.events.On(progress.Event{
|
||||
s.events.On(api.Resource{
|
||||
ID: "Image " + service.Image,
|
||||
Status: progress.Done,
|
||||
Status: api.Done,
|
||||
Text: "Skipped",
|
||||
Details: "Image is already present locally",
|
||||
})
|
||||
@@ -97,9 +96,9 @@ func (s *composeService) pull(ctx context.Context, project *types.Project, opts
|
||||
}
|
||||
|
||||
if service.Build != nil && opts.IgnoreBuildable {
|
||||
s.events.On(progress.Event{
|
||||
s.events.On(api.Resource{
|
||||
ID: "Image " + service.Image,
|
||||
Status: progress.Done,
|
||||
Status: api.Done,
|
||||
Text: "Skipped",
|
||||
Details: "Image can be built",
|
||||
})
|
||||
@@ -122,7 +121,7 @@ func (s *composeService) pull(ctx context.Context, project *types.Project, opts
|
||||
}
|
||||
if !opts.IgnoreFailures && service.Build == nil {
|
||||
if s.dryRun {
|
||||
s.events.On(progress.ErrorEventf("Image "+service.Image,
|
||||
s.events.On(errorEventf("Image "+service.Image,
|
||||
"error pulling image: %s", service.Image))
|
||||
}
|
||||
// fail fast if image can't be pulled nor built
|
||||
@@ -174,7 +173,7 @@ func getUnwrappedErrorMessage(err error) string {
|
||||
|
||||
func (s *composeService) pullServiceImage(ctx context.Context, service types.ServiceConfig, quietPull bool, defaultPlatform string) (string, error) {
|
||||
resource := "Image " + service.Image
|
||||
s.events.On(progress.PullingEvent(service.Image))
|
||||
s.events.On(pullingEvent(service.Image))
|
||||
ref, err := reference.ParseNormalizedNamed(service.Image)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -196,9 +195,9 @@ func (s *composeService) pullServiceImage(ctx context.Context, service types.Ser
|
||||
})
|
||||
|
||||
if ctx.Err() != nil {
|
||||
s.events.On(progress.Event{
|
||||
s.events.On(api.Resource{
|
||||
ID: resource,
|
||||
Status: progress.Warning,
|
||||
Status: api.Warning,
|
||||
Text: "Interrupted",
|
||||
})
|
||||
return "", nil
|
||||
@@ -207,16 +206,16 @@ func (s *composeService) pullServiceImage(ctx context.Context, service types.Ser
|
||||
// check if has error and the service has a build section
|
||||
// then the status should be warning instead of error
|
||||
if err != nil && service.Build != nil {
|
||||
s.events.On(progress.Event{
|
||||
s.events.On(api.Resource{
|
||||
ID: resource,
|
||||
Status: progress.Warning,
|
||||
Status: api.Warning,
|
||||
Text: getUnwrappedErrorMessage(err),
|
||||
})
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
s.events.On(progress.ErrorEvent(resource, getUnwrappedErrorMessage(err)))
|
||||
s.events.On(errorEvent(resource, getUnwrappedErrorMessage(err)))
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -236,7 +235,7 @@ func (s *composeService) pullServiceImage(ctx context.Context, service types.Ser
|
||||
toPullProgressEvent(resource, jm, s.events)
|
||||
}
|
||||
}
|
||||
s.events.On(progress.PulledEvent(service.Image))
|
||||
s.events.On(pulledEvent(service.Image))
|
||||
|
||||
inspected, err := s.apiClient().ImageInspect(ctx, service.Image)
|
||||
if err != nil {
|
||||
@@ -383,7 +382,7 @@ func isServiceImageToBuild(service types.ServiceConfig, services types.Services)
|
||||
|
||||
const (
|
||||
PreparingPhase = "Preparing"
|
||||
WaitingPhase = "Waiting"
|
||||
WaitingPhase = "waiting"
|
||||
PullingFsPhase = "Pulling fs layer"
|
||||
DownloadingPhase = "Downloading"
|
||||
DownloadCompletePhase = "Download complete"
|
||||
@@ -393,7 +392,7 @@ const (
|
||||
PullCompletePhase = "Pull complete"
|
||||
)
|
||||
|
||||
func toPullProgressEvent(parent string, jm jsonmessage.JSONMessage, events progress.EventProcessor) {
|
||||
func toPullProgressEvent(parent string, jm jsonmessage.JSONMessage, events api.EventProcessor) {
|
||||
if jm.ID == "" || jm.Progress == nil {
|
||||
return
|
||||
}
|
||||
@@ -403,7 +402,7 @@ func toPullProgressEvent(parent string, jm jsonmessage.JSONMessage, events progr
|
||||
total int64
|
||||
percent int
|
||||
current int64
|
||||
status = progress.Working
|
||||
status = api.Working
|
||||
)
|
||||
|
||||
text = jm.Progress.String()
|
||||
@@ -420,22 +419,22 @@ func toPullProgressEvent(parent string, jm jsonmessage.JSONMessage, events progr
|
||||
}
|
||||
}
|
||||
case DownloadCompletePhase, AlreadyExistsPhase, PullCompletePhase:
|
||||
status = progress.Done
|
||||
status = api.Done
|
||||
percent = 100
|
||||
}
|
||||
|
||||
if strings.Contains(jm.Status, "Image is up to date") ||
|
||||
strings.Contains(jm.Status, "Downloaded newer image") {
|
||||
status = progress.Done
|
||||
status = api.Done
|
||||
percent = 100
|
||||
}
|
||||
|
||||
if jm.Error != nil {
|
||||
status = progress.Error
|
||||
status = api.Error
|
||||
text = jm.Error.Message
|
||||
}
|
||||
|
||||
events.On(progress.Event{
|
||||
events.On(api.Resource{
|
||||
ID: jm.ID,
|
||||
ParentID: parent,
|
||||
Current: current,
|
||||
|
||||
@@ -33,14 +33,13 @@ import (
|
||||
|
||||
"github.com/docker/compose/v2/internal/registry"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
)
|
||||
|
||||
func (s *composeService) Push(ctx context.Context, project *types.Project, options api.PushOptions) error {
|
||||
if options.Quiet {
|
||||
return s.push(ctx, project, options)
|
||||
}
|
||||
return progress.Run(ctx, func(ctx context.Context) error {
|
||||
return Run(ctx, func(ctx context.Context) error {
|
||||
return s.push(ctx, project, options)
|
||||
}, "push", s.events)
|
||||
}
|
||||
@@ -54,9 +53,9 @@ func (s *composeService) push(ctx context.Context, project *types.Project, optio
|
||||
if options.ImageMandatory && service.Image == "" && service.Provider == nil {
|
||||
return fmt.Errorf("%q attribute is mandatory to push an image for service %q", "service.image", service.Name)
|
||||
}
|
||||
s.events.On(progress.Event{
|
||||
s.events.On(api.Resource{
|
||||
ID: service.Name,
|
||||
Status: progress.Done,
|
||||
Status: api.Done,
|
||||
Text: "Skipped",
|
||||
})
|
||||
continue
|
||||
@@ -68,16 +67,16 @@ func (s *composeService) push(ctx context.Context, project *types.Project, optio
|
||||
|
||||
for _, tag := range tags {
|
||||
eg.Go(func() error {
|
||||
s.events.On(progress.NewEvent(tag, progress.Working, "Pushing"))
|
||||
s.events.On(newEvent(tag, api.Working, "Pushing"))
|
||||
err := s.pushServiceImage(ctx, tag, options.Quiet)
|
||||
if err != nil {
|
||||
if !options.IgnoreFailures {
|
||||
s.events.On(progress.NewEvent(tag, progress.Error, err.Error()))
|
||||
s.events.On(newEvent(tag, api.Error, err.Error()))
|
||||
return err
|
||||
}
|
||||
s.events.On(progress.NewEvent(tag, progress.Warning, err.Error()))
|
||||
s.events.On(newEvent(tag, api.Warning, err.Error()))
|
||||
} else {
|
||||
s.events.On(progress.NewEvent(tag, progress.Done, "Pushed"))
|
||||
s.events.On(newEvent(tag, api.Done, "Pushed"))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@@ -129,24 +128,24 @@ func (s *composeService) pushServiceImage(ctx context.Context, tag string, quiet
|
||||
return nil
|
||||
}
|
||||
|
||||
func toPushProgressEvent(prefix string, jm jsonmessage.JSONMessage, events progress.EventProcessor) {
|
||||
func toPushProgressEvent(prefix string, jm jsonmessage.JSONMessage, events api.EventProcessor) {
|
||||
if jm.ID == "" {
|
||||
// skipped
|
||||
return
|
||||
}
|
||||
var (
|
||||
text string
|
||||
status = progress.Working
|
||||
status = api.Working
|
||||
total int64
|
||||
current int64
|
||||
percent int
|
||||
)
|
||||
if isDone(jm) {
|
||||
status = progress.Done
|
||||
status = api.Done
|
||||
percent = 100
|
||||
}
|
||||
if jm.Error != nil {
|
||||
status = progress.Error
|
||||
status = api.Error
|
||||
text = jm.Error.Message
|
||||
}
|
||||
if jm.Progress != nil {
|
||||
@@ -160,7 +159,7 @@ func toPushProgressEvent(prefix string, jm jsonmessage.JSONMessage, events progr
|
||||
}
|
||||
}
|
||||
|
||||
events.On(progress.Event{
|
||||
events.On(api.Resource{
|
||||
ParentID: prefix,
|
||||
ID: jm.ID,
|
||||
Text: text,
|
||||
|
||||
@@ -24,8 +24,6 @@ import (
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
)
|
||||
|
||||
func (s *composeService) Remove(ctx context.Context, projectName string, options api.RemoveOptions) error {
|
||||
@@ -76,8 +74,7 @@ func (s *composeService) Remove(ctx context.Context, projectName string, options
|
||||
})
|
||||
|
||||
if len(names) == 0 {
|
||||
_, _ = fmt.Fprintln(s.stdinfo(), "No stopped containers")
|
||||
return nil
|
||||
return api.ErrNoResources
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("Going to remove %s", strings.Join(names, ", "))
|
||||
@@ -92,7 +89,7 @@ func (s *composeService) Remove(ctx context.Context, projectName string, options
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return progress.Run(ctx, func(ctx context.Context) error {
|
||||
return Run(ctx, func(ctx context.Context) error {
|
||||
return s.remove(ctx, stoppedContainers, options)
|
||||
}, "remove", s.events)
|
||||
}
|
||||
@@ -102,13 +99,13 @@ func (s *composeService) remove(ctx context.Context, containers Containers, opti
|
||||
for _, ctr := range containers {
|
||||
eg.Go(func() error {
|
||||
eventName := getContainerProgressName(ctr)
|
||||
s.events.On(progress.RemovingEvent(eventName))
|
||||
s.events.On(removingEvent(eventName))
|
||||
err := s.apiClient().ContainerRemove(ctx, ctr.ID, container.RemoveOptions{
|
||||
RemoveVolumes: options.Volumes,
|
||||
Force: options.Force,
|
||||
})
|
||||
if err == nil {
|
||||
s.events.On(progress.RemovedEvent(eventName))
|
||||
s.events.On(removedEvent(eventName))
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
||||
@@ -22,14 +22,13 @@ import (
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
func (s *composeService) Restart(ctx context.Context, projectName string, options api.RestartOptions) error {
|
||||
return progress.Run(ctx, func(ctx context.Context) error {
|
||||
return Run(ctx, func(ctx context.Context) error {
|
||||
return s.restart(ctx, strings.ToLower(projectName), options)
|
||||
}, "restart", s.events)
|
||||
}
|
||||
@@ -93,13 +92,13 @@ func (s *composeService) restart(ctx context.Context, projectName string, option
|
||||
}
|
||||
}
|
||||
eventName := getContainerProgressName(ctr)
|
||||
s.events.On(progress.RestartingEvent(eventName))
|
||||
s.events.On(restartingEvent(eventName))
|
||||
timeout := utils.DurationSecondToInt(options.Timeout)
|
||||
err = s.apiClient().ContainerRestart(ctx, ctr.ID, container.StopOptions{Timeout: timeout})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.events.On(progress.StartedEvent(eventName))
|
||||
s.events.On(startedEvent(eventName))
|
||||
for _, hook := range def.PostStart {
|
||||
err = s.runHook(ctx, ctr, def, hook, nil)
|
||||
if err != nil {
|
||||
|
||||
@@ -28,7 +28,6 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
cmd "github.com/docker/cli/cli/command/container"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
)
|
||||
|
||||
@@ -65,7 +64,7 @@ func (s *composeService) prepareRun(ctx context.Context, project *types.Project,
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = progress.Run(ctx, func(ctx context.Context) error {
|
||||
err = Run(ctx, func(ctx context.Context) error {
|
||||
return s.startDependencies(ctx, project, opts)
|
||||
}, "run", s.events)
|
||||
if err != nil {
|
||||
|
||||
@@ -21,11 +21,10 @@ import (
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/compose/v2/internal/tracing"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
)
|
||||
|
||||
func (s *composeService) Scale(ctx context.Context, project *types.Project, options api.ScaleOptions) error {
|
||||
return progress.Run(ctx, tracing.SpanWrapFunc("project/scale", tracing.ProjectOptions(ctx, project), func(ctx context.Context) error {
|
||||
return Run(ctx, tracing.SpanWrapFunc("project/scale", tracing.ProjectOptions(ctx, project), func(ctx context.Context) error {
|
||||
err := s.create(ctx, project, api.CreateOptions{Services: options.Services})
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
containerType "github.com/docker/docker/api/types/container"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
@@ -31,7 +30,7 @@ import (
|
||||
)
|
||||
|
||||
func (s *composeService) Start(ctx context.Context, projectName string, options api.StartOptions) error {
|
||||
return progress.Run(ctx, func(ctx context.Context) error {
|
||||
return Run(ctx, func(ctx context.Context) error {
|
||||
return s.start(ctx, strings.ToLower(projectName), options, nil)
|
||||
}, "start", s.events)
|
||||
}
|
||||
|
||||
@@ -22,11 +22,10 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
)
|
||||
|
||||
func (s *composeService) Stop(ctx context.Context, projectName string, options api.StopOptions) error {
|
||||
return progress.Run(ctx, func(ctx context.Context) error {
|
||||
return Run(ctx, func(ctx context.Context) error {
|
||||
return s.stop(ctx, strings.ToLower(projectName), options, nil)
|
||||
}, "stop", s.events)
|
||||
}
|
||||
|
||||
@@ -33,14 +33,13 @@ import (
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
"github.com/docker/compose/v2/internal/tracing"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
"github.com/eiannone/keyboard"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
func (s *composeService) Up(ctx context.Context, project *types.Project, options api.UpOptions) error { //nolint:gocyclo
|
||||
err := progress.Run(ctx, tracing.SpanWrapFunc("project/up", tracing.ProjectOptions(ctx, project), func(ctx context.Context) error {
|
||||
err := Run(ctx, tracing.SpanWrapFunc("project/up", tracing.ProjectOptions(ctx, project), func(ctx context.Context) error {
|
||||
err := s.create(ctx, project, options.Create)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -126,7 +125,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
|
||||
first := true
|
||||
gracefulTeardown := func() {
|
||||
first = false
|
||||
fmt.Println("Gracefully Stopping... press Ctrl+C again to force")
|
||||
s.events.On(newEvent(api.ResourceCompose, api.Working, api.StatusStopping, "Gracefully Stopping... press Ctrl+C again to force"))
|
||||
eg.Go(func() error {
|
||||
err = s.stop(context.WithoutCancel(globalCtx), project.Name, api.StopOptions{
|
||||
Services: options.Create.Services,
|
||||
@@ -162,7 +161,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
|
||||
All: true,
|
||||
})
|
||||
// Ignore errors indicating that some of the containers were already stopped or removed.
|
||||
if errdefs.IsNotFound(err) || errdefs.IsConflict(err) {
|
||||
if errdefs.IsNotFound(err) || errdefs.IsConflict(err) || errors.Is(err, api.ErrNoResources) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -205,7 +204,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
|
||||
}
|
||||
once = false
|
||||
exitCode = event.ExitCode
|
||||
_, _ = fmt.Fprintln(s.stdinfo(), progress.ErrorColor("Aborting on container exit..."))
|
||||
s.events.On(newEvent(api.ResourceCompose, api.Working, api.StatusStopping, "Aborting on container exit..."))
|
||||
eg.Go(func() error {
|
||||
err = s.stop(context.WithoutCancel(globalCtx), project.Name, api.StopOptions{
|
||||
Services: options.Create.Services,
|
||||
|
||||
@@ -33,9 +33,9 @@ import (
|
||||
"github.com/docker/compose/v2/internal/sync"
|
||||
"github.com/docker/compose/v2/internal/tracing"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/progress"
|
||||
cutils "github.com/docker/compose/v2/pkg/utils"
|
||||
"github.com/docker/compose/v2/pkg/watch"
|
||||
"github.com/moby/buildkit/util/progress/progressui"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/compose-spec/compose-go/v2/utils"
|
||||
@@ -472,7 +472,7 @@ func (t tarDockerClient) Exec(ctx context.Context, containerID string, cmd []str
|
||||
})
|
||||
}
|
||||
eg.Go(func() error {
|
||||
_, err := io.Copy(t.s.stdinfo(), conn.Reader)
|
||||
_, err := io.Copy(t.s.stdout(), conn.Reader)
|
||||
return err
|
||||
})
|
||||
|
||||
@@ -613,7 +613,7 @@ func (s *composeService) rebuild(ctx context.Context, project *types.Project, se
|
||||
options.LogTo.Log(api.WatchLogger, fmt.Sprintf("Rebuilding service(s) %q after changes were detected...", services))
|
||||
// restrict the build to ONLY this service, not any of its dependencies
|
||||
options.Build.Services = services
|
||||
options.Build.Progress = progress.ModePlain
|
||||
options.Build.Progress = string(progressui.PlainMode)
|
||||
options.Build.Out = cutils.GetWriter(func(line string) {
|
||||
options.LogTo.Log(api.WatchLogger, line)
|
||||
})
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package progress
|
||||
|
||||
import (
|
||||
"github.com/morikuni/aec"
|
||||
)
|
||||
|
||||
type colorFunc func(string) string
|
||||
|
||||
var (
|
||||
nocolor colorFunc = func(s string) string {
|
||||
return s
|
||||
}
|
||||
|
||||
DoneColor colorFunc = aec.BlueF.Apply
|
||||
TimerColor colorFunc = aec.BlueF.Apply
|
||||
CountColor colorFunc = aec.YellowF.Apply
|
||||
WarningColor colorFunc = aec.YellowF.With(aec.Bold).Apply
|
||||
SuccessColor colorFunc = aec.GreenF.Apply
|
||||
ErrorColor colorFunc = aec.RedF.With(aec.Bold).Apply
|
||||
PrefixColor colorFunc = aec.CyanF.Apply
|
||||
)
|
||||
|
||||
func NoColor() {
|
||||
DoneColor = nocolor
|
||||
TimerColor = nocolor
|
||||
CountColor = nocolor
|
||||
WarningColor = nocolor
|
||||
SuccessColor = nocolor
|
||||
ErrorColor = nocolor
|
||||
PrefixColor = nocolor
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package progress
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// EventStatus indicates the status of an action
|
||||
type EventStatus int
|
||||
|
||||
const (
|
||||
// Working means that the current task is working
|
||||
Working EventStatus = iota
|
||||
// Done means that the current task is done
|
||||
Done
|
||||
// Warning means that the current task has warning
|
||||
Warning
|
||||
// Error means that the current task has errored
|
||||
Error
|
||||
)
|
||||
|
||||
const (
|
||||
StatusError = "Error"
|
||||
StatusCreating = "Creating"
|
||||
StatusStarting = "Starting"
|
||||
StatusStarted = "Started"
|
||||
StatusWaiting = "Waiting"
|
||||
StatusHealthy = "Healthy"
|
||||
StatusExited = "Exited"
|
||||
StatusRestarting = "Restarting"
|
||||
StatusRestarted = "Restarted"
|
||||
StatusRunning = "Running"
|
||||
StatusCreated = "Created"
|
||||
StatusStopping = "Stopping"
|
||||
StatusStopped = "Stopped"
|
||||
StatusKilling = "Killing"
|
||||
StatusKilled = "Killed"
|
||||
StatusRemoving = "Removing"
|
||||
StatusRemoved = "Removed"
|
||||
StatusBuilding = "Building"
|
||||
StatusBuilt = "Built"
|
||||
StatusPulling = "Pulling"
|
||||
StatusPulled = "Pulled"
|
||||
StatusCommitting = "Committing"
|
||||
StatusCommitted = "Committed"
|
||||
StatusCopying = "Copying"
|
||||
StatusCopied = "Copied"
|
||||
StatusExporting = "Exporting"
|
||||
StatusExported = "Exported"
|
||||
)
|
||||
|
||||
// Event represents a progress event.
|
||||
type Event struct {
|
||||
ID string
|
||||
ParentID string
|
||||
Text string
|
||||
Details string
|
||||
Status EventStatus
|
||||
Current int64
|
||||
Percent int
|
||||
Total int64
|
||||
}
|
||||
|
||||
func (e *Event) StatusText() string {
|
||||
switch e.Status {
|
||||
case Working:
|
||||
return "Working"
|
||||
case Warning:
|
||||
return "Warning"
|
||||
case Done:
|
||||
return "Done"
|
||||
default:
|
||||
return "Error"
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorEvent creates a new Error Event with message
|
||||
func ErrorEvent(id string, msg string) Event {
|
||||
return Event{
|
||||
ID: id,
|
||||
Status: Error,
|
||||
Text: StatusError,
|
||||
Details: msg,
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorEventf creates a new Error Event with format message
|
||||
func ErrorEventf(id string, msg string, args ...any) Event {
|
||||
return ErrorEvent(id, fmt.Sprintf(msg, args...))
|
||||
}
|
||||
|
||||
// CreatingEvent creates a new Create in progress Event
|
||||
func CreatingEvent(id string) Event {
|
||||
return NewEvent(id, Working, StatusCreating)
|
||||
}
|
||||
|
||||
// StartingEvent creates a new Starting in progress Event
|
||||
func StartingEvent(id string) Event {
|
||||
return NewEvent(id, Working, StatusStarting)
|
||||
}
|
||||
|
||||
// StartedEvent creates a new Started in progress Event
|
||||
func StartedEvent(id string) Event {
|
||||
return NewEvent(id, Done, StatusStarted)
|
||||
}
|
||||
|
||||
// Waiting creates a new waiting event
|
||||
func Waiting(id string) Event {
|
||||
return NewEvent(id, Working, StatusWaiting)
|
||||
}
|
||||
|
||||
// Healthy creates a new healthy event
|
||||
func Healthy(id string) Event {
|
||||
return NewEvent(id, Done, StatusHealthy)
|
||||
}
|
||||
|
||||
// Exited creates a new exited event
|
||||
func Exited(id string) Event {
|
||||
return NewEvent(id, Done, StatusExited)
|
||||
}
|
||||
|
||||
// RestartingEvent creates a new Restarting in progress Event
|
||||
func RestartingEvent(id string) Event {
|
||||
return NewEvent(id, Working, StatusRestarting)
|
||||
}
|
||||
|
||||
// RestartedEvent creates a new Restarted in progress Event
|
||||
func RestartedEvent(id string) Event {
|
||||
return NewEvent(id, Done, StatusRestarted)
|
||||
}
|
||||
|
||||
// RunningEvent creates a new Running in progress Event
|
||||
func RunningEvent(id string) Event {
|
||||
return NewEvent(id, Done, StatusRunning)
|
||||
}
|
||||
|
||||
// CreatedEvent creates a new Created (done) Event
|
||||
func CreatedEvent(id string) Event {
|
||||
return NewEvent(id, Done, StatusCreated)
|
||||
}
|
||||
|
||||
// StoppingEvent creates a new Stopping in progress Event
|
||||
func StoppingEvent(id string) Event {
|
||||
return NewEvent(id, Working, StatusStopping)
|
||||
}
|
||||
|
||||
// StoppedEvent creates a new Stopping in progress Event
|
||||
func StoppedEvent(id string) Event {
|
||||
return NewEvent(id, Done, StatusStopped)
|
||||
}
|
||||
|
||||
// KillingEvent creates a new Killing in progress Event
|
||||
func KillingEvent(id string) Event {
|
||||
return NewEvent(id, Working, StatusKilling)
|
||||
}
|
||||
|
||||
// KilledEvent creates a new Killed in progress Event
|
||||
func KilledEvent(id string) Event {
|
||||
return NewEvent(id, Done, StatusKilled)
|
||||
}
|
||||
|
||||
// RemovingEvent creates a new Removing in progress Event
|
||||
func RemovingEvent(id string) Event {
|
||||
return NewEvent(id, Working, StatusRemoving)
|
||||
}
|
||||
|
||||
// RemovedEvent creates a new removed (done) Event
|
||||
func RemovedEvent(id string) Event {
|
||||
return NewEvent(id, Done, StatusRemoved)
|
||||
}
|
||||
|
||||
// BuildingEvent creates a new Building in progress Event
|
||||
func BuildingEvent(id string) Event {
|
||||
return NewEvent("Image "+id, Working, StatusBuilding)
|
||||
}
|
||||
|
||||
// BuiltEvent creates a new built (done) Event
|
||||
func BuiltEvent(id string) Event {
|
||||
return NewEvent("Image "+id, Done, StatusBuilt)
|
||||
}
|
||||
|
||||
// PullingEvent creates a new pulling (in progress) Event
|
||||
func PullingEvent(id string) Event {
|
||||
return NewEvent("Image "+id, Working, StatusPulling)
|
||||
}
|
||||
|
||||
// PulledEvent creates a new pulled (done) Event
|
||||
func PulledEvent(id string) Event {
|
||||
return NewEvent("Image "+id, Done, StatusPulled)
|
||||
}
|
||||
|
||||
// SkippedEvent creates a new Skipped Event
|
||||
func SkippedEvent(id string, reason string) Event {
|
||||
return Event{
|
||||
ID: id,
|
||||
Status: Warning,
|
||||
Text: "Skipped: " + reason,
|
||||
}
|
||||
}
|
||||
|
||||
// NewEvent new event
|
||||
func NewEvent(id string, status EventStatus, text string) Event {
|
||||
return Event{
|
||||
ID: id,
|
||||
Status: status,
|
||||
Text: text,
|
||||
}
|
||||
}
|
||||
|
||||
// EventProcessor is notified about Compose operations and tasks
|
||||
type EventProcessor interface {
|
||||
// Start is triggered as a Compose operation is starting with context
|
||||
Start(ctx context.Context, operation string)
|
||||
// On notify about (sub)task and progress processing operation
|
||||
On(events ...Event)
|
||||
// Done is triggered as a Compose operation completed
|
||||
Done(operation string, success bool)
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package progress
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
func NewJSONWriter(out io.Writer) EventProcessor {
|
||||
return &jsonWriter{
|
||||
out: out,
|
||||
}
|
||||
}
|
||||
|
||||
type jsonWriter struct {
|
||||
out io.Writer
|
||||
dryRun bool
|
||||
}
|
||||
|
||||
type jsonMessage struct {
|
||||
DryRun bool `json:"dry-run,omitempty"`
|
||||
Tail bool `json:"tail,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
ParentID string `json:"parent_id,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Details string `json:"details,omitempty"`
|
||||
Current int64 `json:"current,omitempty"`
|
||||
Total int64 `json:"total,omitempty"`
|
||||
Percent int `json:"percent,omitempty"`
|
||||
}
|
||||
|
||||
func (p *jsonWriter) Start(ctx context.Context, operation string) {
|
||||
}
|
||||
|
||||
func (p *jsonWriter) Event(e Event) {
|
||||
message := &jsonMessage{
|
||||
DryRun: p.dryRun,
|
||||
Tail: false,
|
||||
ID: e.ID,
|
||||
Status: e.StatusText(),
|
||||
Text: e.Text,
|
||||
Details: e.Details,
|
||||
ParentID: e.ParentID,
|
||||
Current: e.Current,
|
||||
Total: e.Total,
|
||||
Percent: e.Percent,
|
||||
}
|
||||
marshal, err := json.Marshal(message)
|
||||
if err == nil {
|
||||
_, _ = fmt.Fprintln(p.out, string(marshal))
|
||||
}
|
||||
}
|
||||
|
||||
func (p *jsonWriter) On(events ...Event) {
|
||||
for _, e := range events {
|
||||
p.Event(e)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *jsonWriter) Done(_ string, _ bool) {
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package progress
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestJsonWriter_Event(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
w := &jsonWriter{
|
||||
out: &out,
|
||||
dryRun: true,
|
||||
}
|
||||
|
||||
event := Event{
|
||||
ID: "service1",
|
||||
ParentID: "project",
|
||||
Status: Working,
|
||||
Text: StatusCreating,
|
||||
Current: 50,
|
||||
Total: 100,
|
||||
Percent: 50,
|
||||
}
|
||||
w.Event(event)
|
||||
|
||||
var actual jsonMessage
|
||||
err := json.Unmarshal(out.Bytes(), &actual)
|
||||
assert.NilError(t, err)
|
||||
|
||||
expected := jsonMessage{
|
||||
DryRun: true,
|
||||
ID: event.ID,
|
||||
ParentID: event.ParentID,
|
||||
Text: StatusCreating,
|
||||
Status: "Working",
|
||||
Current: event.Current,
|
||||
Total: event.Total,
|
||||
Percent: event.Percent,
|
||||
}
|
||||
assert.DeepEqual(t, expected, actual)
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package progress
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
)
|
||||
|
||||
func NewPlainWriter(out io.Writer) EventProcessor {
|
||||
return &plainWriter{
|
||||
out: out,
|
||||
}
|
||||
}
|
||||
|
||||
type plainWriter struct {
|
||||
out io.Writer
|
||||
dryRun bool
|
||||
}
|
||||
|
||||
func (p *plainWriter) Start(ctx context.Context, operation string) {
|
||||
}
|
||||
|
||||
func (p *plainWriter) Event(e Event) {
|
||||
prefix := ""
|
||||
if p.dryRun {
|
||||
prefix = api.DRYRUN_PREFIX
|
||||
}
|
||||
_, _ = fmt.Fprintln(p.out, prefix, e.ID, e.Text, e.Details)
|
||||
}
|
||||
|
||||
func (p *plainWriter) On(events ...Event) {
|
||||
for _, e := range events {
|
||||
p.Event(e)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *plainWriter) Done(_ string, _ bool) {
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package progress
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type progressFunc func(context.Context) error
|
||||
|
||||
func Run(ctx context.Context, pf progressFunc, operation string, bus EventProcessor) error {
|
||||
bus.Start(ctx, operation)
|
||||
err := pf(ctx)
|
||||
bus.Done(operation, err != nil)
|
||||
return err
|
||||
}
|
||||
|
||||
const (
|
||||
// ModeAuto detect console capabilities
|
||||
ModeAuto = "auto"
|
||||
// ModeTTY use terminal capability for advanced rendering
|
||||
ModeTTY = "tty"
|
||||
// ModePlain dump raw events to output
|
||||
ModePlain = "plain"
|
||||
// ModeQuiet don't display events
|
||||
ModeQuiet = "quiet"
|
||||
// ModeJSON outputs a machine-readable JSON stream
|
||||
ModeJSON = "json"
|
||||
)
|
||||
|
||||
// Mode define how progress should be rendered, either as ModePlain or ModeTTY
|
||||
var Mode = ModeAuto
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package progress
|
||||
|
||||
import "context"
|
||||
|
||||
func NewQuietWriter() EventProcessor {
|
||||
return &quiet{}
|
||||
}
|
||||
|
||||
type quiet struct{}
|
||||
|
||||
func (q *quiet) Start(_ context.Context, _ string) {
|
||||
}
|
||||
|
||||
func (q *quiet) Done(_ string, _ bool) {
|
||||
}
|
||||
|
||||
func (q *quiet) On(_ ...Event) {
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package progress
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Spinner struct {
|
||||
time time.Time
|
||||
index int
|
||||
chars []string
|
||||
stop bool
|
||||
done string
|
||||
}
|
||||
|
||||
func NewSpinner() *Spinner {
|
||||
chars := []string{
|
||||
"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏",
|
||||
}
|
||||
done := "⠿"
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
chars = []string{"-"}
|
||||
done = "-"
|
||||
}
|
||||
|
||||
return &Spinner{
|
||||
index: 0,
|
||||
time: time.Now(),
|
||||
chars: chars,
|
||||
done: done,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Spinner) String() string {
|
||||
if s.stop {
|
||||
return s.done
|
||||
}
|
||||
|
||||
d := time.Since(s.time)
|
||||
if d.Milliseconds() > 100 {
|
||||
s.index = (s.index + 1) % len(s.chars)
|
||||
}
|
||||
|
||||
return s.chars[s.index]
|
||||
}
|
||||
|
||||
func (s *Spinner) Stop() {
|
||||
s.stop = true
|
||||
}
|
||||
|
||||
func (s *Spinner) Restart() {
|
||||
s.stop = false
|
||||
}
|
||||
@@ -1,429 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package progress
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
|
||||
"github.com/buger/goterm"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/morikuni/aec"
|
||||
)
|
||||
|
||||
// NewTTYWriter creates an EventProcessor that render advanced UI within a terminal.
|
||||
// On Start, TUI lists task with a progress timer
|
||||
func NewTTYWriter(out io.Writer) EventProcessor {
|
||||
return &ttyWriter{
|
||||
out: out,
|
||||
tasks: map[string]task{},
|
||||
done: make(chan bool),
|
||||
mtx: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
type ttyWriter struct {
|
||||
out io.Writer
|
||||
ids []string // tasks ids ordered as first event appeared
|
||||
tasks map[string]task
|
||||
repeated bool
|
||||
numLines int
|
||||
done chan bool
|
||||
mtx *sync.Mutex
|
||||
dryRun bool // FIXME(ndeloof) (re)implement support for dry-run
|
||||
skipChildEvents bool
|
||||
operation string
|
||||
ticker *time.Ticker
|
||||
suspended bool
|
||||
}
|
||||
|
||||
type task struct {
|
||||
ID string
|
||||
parentID string
|
||||
startTime time.Time
|
||||
endTime time.Time
|
||||
text string
|
||||
details string
|
||||
status EventStatus
|
||||
current int64
|
||||
percent int
|
||||
total int64
|
||||
spinner *Spinner
|
||||
}
|
||||
|
||||
func (t *task) stop() {
|
||||
t.endTime = time.Now()
|
||||
t.spinner.Stop()
|
||||
}
|
||||
|
||||
func (t *task) hasMore() {
|
||||
t.spinner.Restart()
|
||||
}
|
||||
|
||||
func (w *ttyWriter) Start(ctx context.Context, operation string) {
|
||||
w.ticker = time.NewTicker(100 * time.Millisecond)
|
||||
w.operation = operation
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// interrupted
|
||||
w.ticker.Stop()
|
||||
return
|
||||
case <-w.done:
|
||||
w.print()
|
||||
w.mtx.Lock()
|
||||
w.ticker.Stop()
|
||||
w.operation = ""
|
||||
w.mtx.Unlock()
|
||||
return
|
||||
case <-w.ticker.C:
|
||||
w.print()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (w *ttyWriter) Done(operation string, success bool) {
|
||||
w.done <- true
|
||||
}
|
||||
|
||||
func (w *ttyWriter) On(events ...Event) {
|
||||
w.mtx.Lock()
|
||||
defer w.mtx.Unlock()
|
||||
for _, e := range events {
|
||||
if w.operation != "start" && (e.Text == StatusStarted || e.Text == StatusStarting) {
|
||||
// skip those events to avoid mix with container logs
|
||||
continue
|
||||
}
|
||||
w.event(e)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *ttyWriter) event(e Event) {
|
||||
// Suspend print while a build is in progress, to avoid collision with buildkit Display
|
||||
if e.Text == StatusBuilding {
|
||||
w.ticker.Stop()
|
||||
w.suspended = true
|
||||
} else if w.suspended {
|
||||
w.ticker.Reset(100 * time.Millisecond)
|
||||
w.suspended = false
|
||||
}
|
||||
|
||||
if last, ok := w.tasks[e.ID]; ok {
|
||||
switch e.Status {
|
||||
case Done, Error, Warning:
|
||||
if last.status != e.Status {
|
||||
last.stop()
|
||||
}
|
||||
case Working:
|
||||
last.hasMore()
|
||||
}
|
||||
last.status = e.Status
|
||||
last.text = e.Text
|
||||
last.details = e.Details
|
||||
// progress can only go up
|
||||
if e.Total > last.total {
|
||||
last.total = e.Total
|
||||
}
|
||||
if e.Current > last.current {
|
||||
last.current = e.Current
|
||||
}
|
||||
if e.Percent > last.percent {
|
||||
last.percent = e.Percent
|
||||
}
|
||||
// allow set/unset of parent, but not swapping otherwise prompt is flickering
|
||||
if last.parentID == "" || e.ParentID == "" {
|
||||
last.parentID = e.ParentID
|
||||
}
|
||||
w.tasks[e.ID] = last
|
||||
} else {
|
||||
t := task{
|
||||
ID: e.ID,
|
||||
parentID: e.ParentID,
|
||||
startTime: time.Now(),
|
||||
text: e.Text,
|
||||
details: e.Details,
|
||||
status: e.Status,
|
||||
current: e.Current,
|
||||
percent: e.Percent,
|
||||
total: e.Total,
|
||||
spinner: NewSpinner(),
|
||||
}
|
||||
if e.Status == Done || e.Status == Error {
|
||||
t.stop()
|
||||
}
|
||||
w.tasks[e.ID] = t
|
||||
w.ids = append(w.ids, e.ID)
|
||||
}
|
||||
w.printEvent(e)
|
||||
}
|
||||
|
||||
func (w *ttyWriter) printEvent(e Event) {
|
||||
if w.operation != "" {
|
||||
// event will be displayed by progress UI on ticker's ticks
|
||||
return
|
||||
}
|
||||
|
||||
var color colorFunc
|
||||
switch e.Status {
|
||||
case Working:
|
||||
color = SuccessColor
|
||||
case Done:
|
||||
color = SuccessColor
|
||||
case Warning:
|
||||
color = WarningColor
|
||||
case Error:
|
||||
color = ErrorColor
|
||||
}
|
||||
_, _ = fmt.Fprintf(w.out, "%s %s %s\n", e.ID, color(e.Text), e.Details)
|
||||
}
|
||||
|
||||
func (w *ttyWriter) print() {
|
||||
w.mtx.Lock()
|
||||
defer w.mtx.Unlock()
|
||||
if len(w.tasks) == 0 {
|
||||
return
|
||||
}
|
||||
terminalWidth := goterm.Width()
|
||||
b := aec.EmptyBuilder
|
||||
for i := 0; i <= w.numLines; i++ {
|
||||
b = b.Up(1)
|
||||
}
|
||||
if !w.repeated {
|
||||
b = b.Down(1)
|
||||
}
|
||||
w.repeated = true
|
||||
_, _ = fmt.Fprint(w.out, b.Column(0).ANSI)
|
||||
|
||||
// Hide the cursor while we are printing
|
||||
_, _ = fmt.Fprint(w.out, aec.Hide)
|
||||
defer func() {
|
||||
_, _ = fmt.Fprint(w.out, aec.Show)
|
||||
}()
|
||||
|
||||
firstLine := fmt.Sprintf("[+] %s %d/%d", w.operation, numDone(w.tasks), len(w.tasks))
|
||||
_, _ = fmt.Fprintln(w.out, firstLine)
|
||||
|
||||
var statusPadding int
|
||||
for _, t := range w.tasks {
|
||||
l := len(t.ID)
|
||||
if statusPadding < l {
|
||||
statusPadding = l
|
||||
}
|
||||
if t.parentID != "" {
|
||||
statusPadding -= 2
|
||||
}
|
||||
}
|
||||
|
||||
if len(w.tasks) > goterm.Height()-2 {
|
||||
w.skipChildEvents = true
|
||||
}
|
||||
numLines := 0
|
||||
|
||||
for _, id := range w.ids { // iterate on ids to enforce a consistent order
|
||||
t := w.tasks[id]
|
||||
if t.parentID != "" {
|
||||
continue
|
||||
}
|
||||
line := w.lineText(t, "", terminalWidth, statusPadding, w.dryRun)
|
||||
_, _ = fmt.Fprint(w.out, line)
|
||||
numLines++
|
||||
for _, t := range w.tasks {
|
||||
if t.parentID == t.ID {
|
||||
if w.skipChildEvents {
|
||||
continue
|
||||
}
|
||||
line := w.lineText(t, " ", terminalWidth, statusPadding, w.dryRun)
|
||||
_, _ = fmt.Fprint(w.out, line)
|
||||
numLines++
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := numLines; i < w.numLines; i++ {
|
||||
if numLines < goterm.Height()-2 {
|
||||
_, _ = fmt.Fprintln(w.out, strings.Repeat(" ", terminalWidth))
|
||||
numLines++
|
||||
}
|
||||
}
|
||||
w.numLines = numLines
|
||||
}
|
||||
|
||||
func (w *ttyWriter) lineText(t task, pad string, terminalWidth, statusPadding int, dryRun bool) string {
|
||||
endTime := time.Now()
|
||||
if t.status != Working {
|
||||
endTime = t.startTime
|
||||
if (t.endTime != time.Time{}) {
|
||||
endTime = t.endTime
|
||||
}
|
||||
}
|
||||
prefix := ""
|
||||
if dryRun {
|
||||
prefix = PrefixColor(api.DRYRUN_PREFIX)
|
||||
}
|
||||
|
||||
elapsed := endTime.Sub(t.startTime).Seconds()
|
||||
|
||||
var (
|
||||
hideDetails bool
|
||||
total int64
|
||||
current int64
|
||||
completion []string
|
||||
)
|
||||
|
||||
// only show the aggregated progress while the root operation is in-progress
|
||||
if parent := t; parent.status == Working {
|
||||
for _, id := range w.ids {
|
||||
child := w.tasks[id]
|
||||
if child.parentID == parent.ID {
|
||||
if child.status == Working && child.total == 0 {
|
||||
// we don't have totals available for all the child events
|
||||
// so don't show the total progress yet
|
||||
hideDetails = true
|
||||
}
|
||||
total += child.total
|
||||
current += child.current
|
||||
completion = append(completion, percentChars[(len(percentChars)-1)*child.percent/100])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// don't try to show detailed progress if we don't have any idea
|
||||
if total == 0 {
|
||||
hideDetails = true
|
||||
}
|
||||
|
||||
txt := t.ID
|
||||
if len(completion) > 0 {
|
||||
var progress string
|
||||
if !hideDetails {
|
||||
progress = fmt.Sprintf(" %7s / %-7s", units.HumanSize(float64(current)), units.HumanSize(float64(total)))
|
||||
}
|
||||
txt = fmt.Sprintf("%s [%s]%s",
|
||||
t.ID,
|
||||
SuccessColor(strings.Join(completion, "")),
|
||||
progress,
|
||||
)
|
||||
}
|
||||
textLen := len(txt)
|
||||
padding := statusPadding - textLen
|
||||
if padding < 0 {
|
||||
padding = 0
|
||||
}
|
||||
// calculate the max length for the status text, on errors it
|
||||
// is 2-3 lines long and breaks the line formatting
|
||||
maxDetailsLen := terminalWidth - textLen - statusPadding - 15
|
||||
details := t.details
|
||||
// in some cases (debugging under VS Code), terminalWidth is set to zero by goterm.Width() ; ensuring we don't tweak strings with negative char index
|
||||
if maxDetailsLen > 0 && len(details) > maxDetailsLen {
|
||||
details = details[:maxDetailsLen] + "..."
|
||||
}
|
||||
text := fmt.Sprintf("%s %s%s %s %s%s %s",
|
||||
pad,
|
||||
spinner(t),
|
||||
prefix,
|
||||
txt,
|
||||
strings.Repeat(" ", padding),
|
||||
colorFn(t.status)(t.text),
|
||||
details,
|
||||
)
|
||||
timer := fmt.Sprintf("%.1fs ", elapsed)
|
||||
o := align(text, TimerColor(timer), terminalWidth)
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
var (
|
||||
spinnerDone = "✔"
|
||||
spinnerWarning = "!"
|
||||
spinnerError = "✘"
|
||||
)
|
||||
|
||||
func spinner(t task) string {
|
||||
switch t.status {
|
||||
case Done:
|
||||
return SuccessColor(spinnerDone)
|
||||
case Warning:
|
||||
return WarningColor(spinnerWarning)
|
||||
case Error:
|
||||
return ErrorColor(spinnerError)
|
||||
default:
|
||||
return CountColor(t.spinner.String())
|
||||
}
|
||||
}
|
||||
|
||||
func colorFn(s EventStatus) colorFunc {
|
||||
switch s {
|
||||
case Done:
|
||||
return SuccessColor
|
||||
case Warning:
|
||||
return WarningColor
|
||||
case Error:
|
||||
return ErrorColor
|
||||
default:
|
||||
return nocolor
|
||||
}
|
||||
}
|
||||
|
||||
func numDone(tasks map[string]task) int {
|
||||
i := 0
|
||||
for _, t := range tasks {
|
||||
if t.status != Working {
|
||||
i++
|
||||
}
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func align(l, r string, w int) string {
|
||||
ll := lenAnsi(l)
|
||||
lr := lenAnsi(r)
|
||||
pad := ""
|
||||
count := w - ll - lr
|
||||
if count > 0 {
|
||||
pad = strings.Repeat(" ", count)
|
||||
}
|
||||
return fmt.Sprintf("%s%s%s\n", l, pad, r)
|
||||
}
|
||||
|
||||
// lenAnsi count of user-perceived characters in ANSI string.
|
||||
func lenAnsi(s string) int {
|
||||
length := 0
|
||||
ansiCode := false
|
||||
for _, r := range s {
|
||||
if r == '\x1b' {
|
||||
ansiCode = true
|
||||
continue
|
||||
}
|
||||
if ansiCode && r == 'm' {
|
||||
ansiCode = false
|
||||
continue
|
||||
}
|
||||
if !ansiCode {
|
||||
length++
|
||||
}
|
||||
}
|
||||
return length
|
||||
}
|
||||
|
||||
var percentChars = strings.Split("⠀⡀⣀⣄⣤⣦⣶⣷⣿", "")
|
||||
Reference in New Issue
Block a user