2024-11-11 22:05:56 +00:00
|
|
|
package compose
|
2023-07-13 20:55:52 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/portainer/portainer/pkg/libstack"
|
2023-10-24 16:55:11 +00:00
|
|
|
|
2024-11-11 22:05:56 +00:00
|
|
|
"github.com/compose-spec/compose-go/v2/types"
|
|
|
|
"github.com/docker/compose/v2/pkg/api"
|
2023-07-13 20:55:52 +00:00
|
|
|
"github.com/rs/zerolog/log"
|
|
|
|
)
|
|
|
|
|
|
|
|
type publisher struct {
|
|
|
|
URL string
|
|
|
|
TargetPort int
|
|
|
|
PublishedPort int
|
|
|
|
Protocol string
|
|
|
|
}
|
|
|
|
|
|
|
|
type service struct {
|
|
|
|
ID string
|
|
|
|
Name string
|
|
|
|
Image string
|
|
|
|
Command string
|
|
|
|
Project string
|
|
|
|
Service string
|
|
|
|
Created int64
|
|
|
|
State string
|
|
|
|
Status string
|
|
|
|
Health string
|
|
|
|
ExitCode int
|
|
|
|
Publishers []publisher
|
|
|
|
}
|
|
|
|
|
|
|
|
// docker container state can be one of "created", "running", "paused", "restarting", "removing", "exited", or "dead"
|
|
|
|
func getServiceStatus(service service) (libstack.Status, string) {
|
|
|
|
log.Debug().
|
|
|
|
Str("service", service.Name).
|
|
|
|
Str("state", service.State).
|
|
|
|
Int("exitCode", service.ExitCode).
|
|
|
|
Msg("getServiceStatus")
|
|
|
|
|
|
|
|
switch service.State {
|
|
|
|
case "created", "restarting", "paused":
|
|
|
|
return libstack.StatusStarting, ""
|
|
|
|
case "running":
|
|
|
|
return libstack.StatusRunning, ""
|
|
|
|
case "removing":
|
|
|
|
return libstack.StatusRemoving, ""
|
2024-04-30 01:44:08 +00:00
|
|
|
case "exited":
|
|
|
|
if service.ExitCode != 0 {
|
|
|
|
return libstack.StatusError, fmt.Sprintf("service %s exited with code %d", service.Name, service.ExitCode)
|
|
|
|
}
|
|
|
|
return libstack.StatusCompleted, ""
|
|
|
|
case "dead":
|
2023-07-13 20:55:52 +00:00
|
|
|
if service.ExitCode != 0 {
|
|
|
|
return libstack.StatusError, fmt.Sprintf("service %s exited with code %d", service.Name, service.ExitCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
return libstack.StatusRemoved, ""
|
|
|
|
default:
|
|
|
|
return libstack.StatusUnknown, ""
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func aggregateStatuses(services []service) (libstack.Status, string) {
|
|
|
|
servicesCount := len(services)
|
|
|
|
|
|
|
|
if servicesCount == 0 {
|
|
|
|
log.Debug().
|
|
|
|
Msg("no services found")
|
|
|
|
|
|
|
|
return libstack.StatusRemoved, ""
|
|
|
|
}
|
|
|
|
|
|
|
|
statusCounts := make(map[libstack.Status]int)
|
|
|
|
errorMessage := ""
|
|
|
|
for _, service := range services {
|
|
|
|
status, serviceError := getServiceStatus(service)
|
|
|
|
if serviceError != "" {
|
|
|
|
errorMessage = serviceError
|
|
|
|
}
|
|
|
|
statusCounts[status]++
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Debug().
|
|
|
|
Interface("statusCounts", statusCounts).
|
|
|
|
Str("errorMessage", errorMessage).
|
|
|
|
Msg("check_status")
|
|
|
|
|
|
|
|
switch {
|
|
|
|
case errorMessage != "":
|
|
|
|
return libstack.StatusError, errorMessage
|
|
|
|
case statusCounts[libstack.StatusStarting] > 0:
|
|
|
|
return libstack.StatusStarting, ""
|
|
|
|
case statusCounts[libstack.StatusRemoving] > 0:
|
|
|
|
return libstack.StatusRemoving, ""
|
2024-04-30 01:44:08 +00:00
|
|
|
case statusCounts[libstack.StatusCompleted] == servicesCount:
|
|
|
|
return libstack.StatusCompleted, ""
|
|
|
|
case statusCounts[libstack.StatusRunning]+statusCounts[libstack.StatusCompleted] == servicesCount:
|
2023-07-13 20:55:52 +00:00
|
|
|
return libstack.StatusRunning, ""
|
|
|
|
case statusCounts[libstack.StatusStopped] == servicesCount:
|
|
|
|
return libstack.StatusStopped, ""
|
|
|
|
case statusCounts[libstack.StatusRemoved] == servicesCount:
|
|
|
|
return libstack.StatusRemoved, ""
|
|
|
|
default:
|
|
|
|
return libstack.StatusUnknown, ""
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2024-11-13 03:30:08 +00:00
|
|
|
func (c *ComposeDeployer) WaitForStatus(ctx context.Context, name string, status libstack.Status) <-chan libstack.WaitResult {
|
2024-04-30 01:44:08 +00:00
|
|
|
waitResultCh := make(chan libstack.WaitResult)
|
2024-11-11 22:05:56 +00:00
|
|
|
waitResult := libstack.WaitResult{Status: status}
|
2023-07-13 20:55:52 +00:00
|
|
|
|
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
2024-10-10 15:06:20 +00:00
|
|
|
waitResult.ErrorMsg = "failed to wait for status: " + ctx.Err().Error()
|
2024-04-30 01:44:08 +00:00
|
|
|
waitResultCh <- waitResult
|
2023-07-13 20:55:52 +00:00
|
|
|
default:
|
|
|
|
}
|
|
|
|
|
|
|
|
time.Sleep(1 * time.Second)
|
|
|
|
|
2024-11-11 22:05:56 +00:00
|
|
|
var containerSummaries []api.ContainerSummary
|
2024-02-29 22:50:20 +00:00
|
|
|
|
2024-11-11 22:05:56 +00:00
|
|
|
if err := withComposeService(ctx, nil, libstack.Options{ProjectName: name}, func(composeService api.Service, project *types.Project) error {
|
|
|
|
var err error
|
2024-11-12 22:31:44 +00:00
|
|
|
|
|
|
|
psCtx, cancelFunc := context.WithTimeout(context.Background(), time.Minute)
|
|
|
|
defer cancelFunc()
|
|
|
|
containerSummaries, err = composeService.Ps(psCtx, name, api.PsOptions{All: true})
|
|
|
|
|
2024-11-11 22:05:56 +00:00
|
|
|
return err
|
|
|
|
}); err != nil {
|
2023-07-13 20:55:52 +00:00
|
|
|
log.Debug().
|
|
|
|
Str("project_name", name).
|
|
|
|
Err(err).
|
|
|
|
Msg("error from docker compose ps")
|
2024-11-11 22:05:56 +00:00
|
|
|
|
2023-07-13 20:55:52 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2024-11-11 22:05:56 +00:00
|
|
|
services := serviceListFromContainerSummary(containerSummaries)
|
2023-07-13 20:55:52 +00:00
|
|
|
|
|
|
|
if len(services) == 0 && status == libstack.StatusRemoved {
|
2024-04-30 01:44:08 +00:00
|
|
|
waitResultCh <- waitResult
|
2023-07-13 20:55:52 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
aggregateStatus, errorMessage := aggregateStatuses(services)
|
|
|
|
if aggregateStatus == status {
|
2024-04-30 01:44:08 +00:00
|
|
|
waitResultCh <- waitResult
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if status == libstack.StatusRunning && aggregateStatus == libstack.StatusCompleted {
|
|
|
|
waitResult.Status = libstack.StatusCompleted
|
|
|
|
waitResultCh <- waitResult
|
2023-07-13 20:55:52 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if errorMessage != "" {
|
2024-04-30 01:44:08 +00:00
|
|
|
waitResult.ErrorMsg = errorMessage
|
|
|
|
waitResultCh <- waitResult
|
2023-07-13 20:55:52 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Debug().
|
|
|
|
Str("project_name", name).
|
2024-11-11 13:02:20 +00:00
|
|
|
Str("required_status", string(status)).
|
2023-07-13 20:55:52 +00:00
|
|
|
Str("status", string(aggregateStatus)).
|
|
|
|
Msg("waiting for status")
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2024-04-30 01:44:08 +00:00
|
|
|
return waitResultCh
|
2023-07-13 20:55:52 +00:00
|
|
|
}
|
2024-11-11 22:05:56 +00:00
|
|
|
|
|
|
|
func serviceListFromContainerSummary(containerSummaries []api.ContainerSummary) []service {
|
|
|
|
var services []service
|
|
|
|
|
|
|
|
for _, cs := range containerSummaries {
|
|
|
|
var publishers []publisher
|
|
|
|
|
|
|
|
for _, p := range cs.Publishers {
|
|
|
|
publishers = append(publishers, publisher{
|
|
|
|
URL: p.URL,
|
|
|
|
TargetPort: p.TargetPort,
|
|
|
|
PublishedPort: p.PublishedPort,
|
|
|
|
Protocol: p.Protocol,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
services = append(services, service{
|
|
|
|
ID: cs.ID,
|
|
|
|
Name: cs.Name,
|
|
|
|
Image: cs.Image,
|
|
|
|
Command: cs.Command,
|
|
|
|
Project: cs.Project,
|
|
|
|
Service: cs.Service,
|
|
|
|
Created: cs.Created,
|
|
|
|
State: cs.State,
|
|
|
|
Status: cs.Status,
|
|
|
|
Health: cs.Health,
|
|
|
|
ExitCode: cs.ExitCode,
|
|
|
|
Publishers: publishers,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return services
|
|
|
|
}
|