mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-11-15 08:05:47 +00:00
fix(container): reject ambiguous compose projects when cleaning app logs
When several projects use the same compose file, pick the project whose name matches the request name, exactly or after normalization. If none matches, return an error instead of the first project in the container list, so log cleaning never stops and truncates another project.
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -608,7 +609,10 @@ func (u *ContainerService) ComposeLogClean(req dto.ComposeLogClean) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
projectName := loadComposeProjectNameForLog(list, req.Name, req.Path)
|
||||
projectName, err := loadComposeProjectNameForLog(list, req.Name, req.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx := context.Background()
|
||||
for _, item := range list {
|
||||
if name, ok := item.Labels[composeProjectLabel]; ok {
|
||||
@@ -648,24 +652,37 @@ func (u *ContainerService) ComposeLogClean(req dto.ComposeLogClean) error {
|
||||
// loadComposeProjectNameForLog returns the project name that Compose uses for
|
||||
// the containers of composePath. Apps and runtimes pass their display name,
|
||||
// which can differ from the project name, for example in letter case.
|
||||
func loadComposeProjectNameForLog(list []container.Summary, name, composePath string) string {
|
||||
// If several projects use composePath and none of them matches name, it
|
||||
// returns an error rather than picking one of them.
|
||||
func loadComposeProjectNameForLog(list []container.Summary, name, composePath string) (string, error) {
|
||||
for _, item := range list {
|
||||
if item.Labels[composeProjectLabel] == name {
|
||||
return name
|
||||
return name, nil
|
||||
}
|
||||
}
|
||||
target := filepath.Clean(strings.TrimSpace(primaryComposePath(composePath)))
|
||||
var candidates []string
|
||||
for _, item := range list {
|
||||
projectName := item.Labels[composeProjectLabel]
|
||||
if projectName == "" {
|
||||
if projectName == "" || slices.Contains(candidates, projectName) {
|
||||
continue
|
||||
}
|
||||
configFile := strings.TrimSpace(primaryComposePath(item.Labels[composeConfigLabel]))
|
||||
if configFile != "" && filepath.Clean(configFile) == target {
|
||||
return projectName
|
||||
candidates = append(candidates, projectName)
|
||||
}
|
||||
}
|
||||
return name
|
||||
switch len(candidates) {
|
||||
case 0:
|
||||
return name, nil
|
||||
case 1:
|
||||
return candidates[0], nil
|
||||
}
|
||||
if normalized := normalizeComposeProjectName(name); slices.Contains(candidates, normalized) {
|
||||
return normalized, nil
|
||||
}
|
||||
slices.Sort(candidates)
|
||||
return "", fmt.Errorf("compose file %s is used by multiple projects (%s), cannot determine the project of %s", target, strings.Join(candidates, ", "), name)
|
||||
}
|
||||
|
||||
func (u *ContainerService) LoadComposeEnv(name string) (string, error) {
|
||||
|
||||
@@ -3,11 +3,16 @@ package service
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/re"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
)
|
||||
|
||||
func TestLoadComposeProjectNameForLog(t *testing.T) {
|
||||
list := []container.Summary{
|
||||
{Labels: map[string]string{
|
||||
composeProjectLabel: "openclaw",
|
||||
composeConfigLabel: "/opt/1panel/apps/openclaw/OpenClaw/docker-compose.yml",
|
||||
}},
|
||||
{Labels: map[string]string{
|
||||
composeProjectLabel: "openclaw",
|
||||
composeConfigLabel: "/opt/1panel/apps/openclaw/OpenClaw/docker-compose.yml",
|
||||
@@ -30,9 +35,49 @@ func TestLoadComposeProjectNameForLog(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := loadComposeProjectNameForLog(list, tt.inputName, tt.composePath); got != tt.want {
|
||||
got, err := loadComposeProjectNameForLog(list, tt.inputName, tt.composePath)
|
||||
if err != nil {
|
||||
t.Fatalf("loadComposeProjectNameForLog(%q, %q) returned error: %v", tt.inputName, tt.composePath, err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Fatalf("loadComposeProjectNameForLog(%q, %q) = %q, want %q", tt.inputName, tt.composePath, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadComposeProjectNameForLogSharedComposeFile(t *testing.T) {
|
||||
re.Init()
|
||||
const composePath = "/opt/1panel/apps/openclaw/OpenClaw/docker-compose.yml"
|
||||
openclaw := container.Summary{Labels: map[string]string{
|
||||
composeProjectLabel: "openclaw",
|
||||
composeConfigLabel: composePath,
|
||||
}}
|
||||
sandbox := container.Summary{Labels: map[string]string{
|
||||
composeProjectLabel: "sandbox",
|
||||
composeConfigLabel: composePath,
|
||||
}}
|
||||
orders := map[string][]container.Summary{
|
||||
"openclaw first": {openclaw, sandbox},
|
||||
"sandbox first": {sandbox, openclaw},
|
||||
}
|
||||
for order, list := range orders {
|
||||
t.Run(order, func(t *testing.T) {
|
||||
// The display name normalizes to one of the projects.
|
||||
got, err := loadComposeProjectNameForLog(list, "OpenClaw", composePath)
|
||||
if err != nil || got != "openclaw" {
|
||||
t.Fatalf("display name: got %q, %v, want %q", got, err, "openclaw")
|
||||
}
|
||||
// An exact project name selects that project.
|
||||
got, err = loadComposeProjectNameForLog(list, "sandbox", composePath)
|
||||
if err != nil || got != "sandbox" {
|
||||
t.Fatalf("project name: got %q, %v, want %q", got, err, "sandbox")
|
||||
}
|
||||
// A name that matches neither project is ambiguous.
|
||||
got, err = loadComposeProjectNameForLog(list, "My App", composePath)
|
||||
if err == nil {
|
||||
t.Fatalf("ambiguous name: got %q, want an error", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user