mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 07:41:27 +00:00
fix: polish service runtime UX and harden reconcile edge cases
Backend: - tolerate concurrent project-network creation (re-resolve on conflict) - always publish an ephemeral host port when a port is configured, even with a domain set — Traefik routes on top when the edge network exists - public_url now prefers the configured domain, falls back to the first published port - service logs resolve replicas by containr.service label and merge all replica streams — previously queried a nonexistent containr-<id> name Canvas/detail: - degraded status renders amber end-to-end (node card, detail badge, status dot) instead of falling through to "Stopped" - edges pick handle sides by relative node position — no more backward loops when the source sits right of the target - context menu gains Start for stopped services, Visit uses the published URL when no domain is set, menu clamps to canvas bounds - node cards show the image tag instead of a redundant hint - scaling tab falls back to live runtime replica counts when no autoscaler state exists; manual scale no longer requires it Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
04c3b99531
commit
b7e2e93261
9 files changed
+193
-69
No files matched your search
@@ -8,9 +8,11 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
@@ -73,7 +75,21 @@ func handleGetLogs(c *gin.Context) {
|
||||
respondEmptyServiceLogs(c, "Container log streaming is unavailable because Docker is not configured.")
|
||||
return
|
||||
}
|
||||
containerName := fmt.Sprintf("containr-%s", serviceID)
|
||||
// Replicas are named containr-<serviceID>-<n>; resolve them by label and
|
||||
// merge logs across all of them.
|
||||
ctx := context.Background()
|
||||
containers, err := client.ListContainersFiltered(ctx, filters.NewArgs(
|
||||
filters.Arg("label", "containr.service="+serviceID.String()),
|
||||
), false)
|
||||
if err != nil {
|
||||
respondDependencyUnavailable(c, "docker", "Failed to fetch container logs. The container may be unavailable.")
|
||||
return
|
||||
}
|
||||
if len(containers) == 0 {
|
||||
respondEmptyServiceLogs(c, "No container logs are available for this service yet.")
|
||||
return
|
||||
}
|
||||
sort.Slice(containers, func(i, j int) bool { return containers[i].Names[0] < containers[j].Names[0] })
|
||||
|
||||
logOpts := docker.LogOptions{
|
||||
Stdout: true,
|
||||
@@ -83,17 +99,25 @@ func handleGetLogs(c *gin.Context) {
|
||||
Timestamps: true,
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
logsReader, err := client.GetContainerLogs(ctx, containerName, logOpts)
|
||||
if err != nil {
|
||||
if isDockerNotFoundError(err) {
|
||||
respondEmptyServiceLogs(c, "No container logs are available for this service yet.")
|
||||
return
|
||||
var readers []io.ReadCloser
|
||||
var streams []io.Reader
|
||||
for _, ctr := range containers {
|
||||
r, rerr := client.GetContainerLogs(ctx, ctr.ID, logOpts)
|
||||
if rerr == nil {
|
||||
readers = append(readers, r)
|
||||
streams = append(streams, r)
|
||||
}
|
||||
respondDependencyUnavailable(c, "docker", "Failed to fetch container logs. The container may be unavailable.")
|
||||
}
|
||||
if len(readers) == 0 {
|
||||
respondEmptyServiceLogs(c, "No container logs are available for this service yet.")
|
||||
return
|
||||
}
|
||||
defer logsReader.Close()
|
||||
logsReader := io.MultiReader(streams...)
|
||||
defer func() {
|
||||
for _, r := range readers {
|
||||
_ = r.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
if follow {
|
||||
c.Header("Content-Type", "text/event-stream")
|
||||
|
||||
Reference in new issue
Block a user