mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-16 14:57:05 +08:00
fix(compose): handle relative paths and preserve multi-file projects (#14062)
This commit is contained in:
@@ -371,43 +371,39 @@ func loadComposePathAndFiles(composeName string, dockerClient *client.Client) (s
|
||||
}
|
||||
config := targetContainer.Labels[composeConfigLabel]
|
||||
workdir := targetContainer.Labels[composeWorkdirLabel]
|
||||
if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) {
|
||||
composePath = config
|
||||
} else {
|
||||
composePath = workdir
|
||||
files, err := resolveComposeFiles(workdir, config)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
composePath = strings.Join(files, ",")
|
||||
}
|
||||
composeFiles := normalizeComposeFiles(composePath)
|
||||
if len(composeFiles) == 0 {
|
||||
return "", nil, fmt.Errorf("compose file not found for %s", composeName)
|
||||
composeFiles, err := normalizeComposeFiles(composePath)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return composePath, composeFiles, nil
|
||||
return strings.Join(composeFiles, ","), composeFiles, nil
|
||||
}
|
||||
|
||||
func normalizeComposeFiles(composePath string) []string {
|
||||
items := strings.Split(composePath, ",")
|
||||
result := make([]string, 0)
|
||||
seen := make(map[string]struct{})
|
||||
for _, item := range items {
|
||||
item = strings.TrimSpace(item)
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
stat, err := os.Stat(item)
|
||||
if err == nil && stat.IsDir() {
|
||||
item = path.Join(item, "docker-compose.yml")
|
||||
}
|
||||
if _, err := os.Stat(item); err != nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[item]; ok {
|
||||
continue
|
||||
}
|
||||
seen[item] = struct{}{}
|
||||
result = append(result, item)
|
||||
func normalizeComposeFiles(composePath string) ([]string, error) {
|
||||
files, err := resolveComposeFiles("", composePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result
|
||||
for i, file := range files {
|
||||
info, err := os.Stat(file)
|
||||
if err == nil && info.IsDir() {
|
||||
file = filepath.Join(file, "docker-compose.yml")
|
||||
info, err = os.Stat(file)
|
||||
files[i] = file
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return nil, fmt.Errorf("compose path %s is not a regular file", file)
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (c *composeBackupContext) close() {
|
||||
@@ -621,20 +617,46 @@ func safeJoinWithinBase(baseDir, name string) (string, error) {
|
||||
}
|
||||
|
||||
func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
|
||||
if recoverCtx.targetDir != "" {
|
||||
_ = os.RemoveAll(recoverCtx.targetDir)
|
||||
composeFiles := recoverCtx.meta.Files
|
||||
if strings.TrimSpace(recoverCtx.meta.ComposePath) != "" {
|
||||
filesByPath := make(map[string]composeBackupFile, len(composeFiles))
|
||||
for _, item := range composeFiles {
|
||||
filesByPath[filepath.Clean(item.OriginalPath)] = item
|
||||
}
|
||||
orderedFiles := make([]composeBackupFile, 0, len(composeFiles))
|
||||
for _, file := range strings.Split(recoverCtx.meta.ComposePath, ",") {
|
||||
file = strings.TrimSpace(file)
|
||||
if file == "" {
|
||||
continue
|
||||
}
|
||||
file = filepath.Clean(file)
|
||||
item, ok := filesByPath[file]
|
||||
if !ok {
|
||||
if len(composeFiles) != 1 || file != filepath.Dir(composeFiles[0].OriginalPath) {
|
||||
return fmt.Errorf("compose file %s is missing from backup metadata", file)
|
||||
}
|
||||
item = composeFiles[0]
|
||||
}
|
||||
orderedFiles = append(orderedFiles, item)
|
||||
}
|
||||
composeFiles = orderedFiles
|
||||
}
|
||||
if err := os.MkdirAll(recoverCtx.targetDir, os.ModePerm); err != nil {
|
||||
return err
|
||||
if len(composeFiles) == 0 {
|
||||
return fmt.Errorf("compose files are missing from backup metadata")
|
||||
}
|
||||
restored := make([]string, 0, len(recoverCtx.meta.Files))
|
||||
for _, item := range recoverCtx.meta.Files {
|
||||
restored := make([]string, 0, len(composeFiles))
|
||||
sourceByTarget := make(map[string]string, len(composeFiles))
|
||||
for _, item := range composeFiles {
|
||||
backupPath, err := safeJoinWithinBase(recoverCtx.tmpDir, item.BackupPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid compose backup path %q, err: %v", item.BackupPath, err)
|
||||
}
|
||||
if !recoverCtx.fileOp.Stat(backupPath) {
|
||||
continue
|
||||
info, err := os.Stat(backupPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return fmt.Errorf("compose backup path %s is not a regular file", backupPath)
|
||||
}
|
||||
targetName := item.FileName
|
||||
if item.RelativePath != "" {
|
||||
@@ -650,6 +672,29 @@ func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid compose target path %q, err: %v", targetName, err)
|
||||
}
|
||||
sourcePath := item.OriginalPath
|
||||
if sourcePath == "" {
|
||||
sourcePath = backupPath
|
||||
}
|
||||
sourcePath = filepath.Clean(sourcePath)
|
||||
if previous, ok := sourceByTarget[targetPath]; ok && previous != sourcePath {
|
||||
return fmt.Errorf("compose files %q and %q resolve to the same restore path %q", previous, sourcePath, targetPath)
|
||||
}
|
||||
sourceByTarget[targetPath] = sourcePath
|
||||
restored = append(restored, targetPath)
|
||||
}
|
||||
if recoverCtx.targetDir != "" {
|
||||
_ = os.RemoveAll(recoverCtx.targetDir)
|
||||
}
|
||||
if err := os.MkdirAll(recoverCtx.targetDir, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
for i, item := range composeFiles {
|
||||
backupPath, err := safeJoinWithinBase(recoverCtx.tmpDir, item.BackupPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid compose backup path %q, err: %v", item.BackupPath, err)
|
||||
}
|
||||
targetPath := restored[i]
|
||||
if err := os.MkdirAll(path.Dir(targetPath), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -660,7 +705,6 @@ func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
|
||||
if err := recoverCtx.fileOp.SaveFile(targetPath, string(content), fs.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
restored = append(restored, targetPath)
|
||||
}
|
||||
envPath := path.Join(recoverCtx.tmpDir, "compose_files", ".env")
|
||||
if recoverCtx.fileOp.Stat(envPath) {
|
||||
@@ -672,14 +716,6 @@ func stepRestoreComposeFiles(recoverCtx *composeRecoverContext) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(restored) == 0 {
|
||||
defaultPath := path.Join(recoverCtx.targetDir, "docker-compose.yml")
|
||||
if !recoverCtx.fileOp.Stat(defaultPath) {
|
||||
return fmt.Errorf("compose file not found in backup data")
|
||||
}
|
||||
restored = append(restored, defaultPath)
|
||||
}
|
||||
sort.Strings(restored)
|
||||
recoverCtx.composePath = strings.Join(restored, ",")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -326,40 +326,47 @@ func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) {
|
||||
case "container":
|
||||
inspectInfo, err = client.ContainerInspect(context.Background(), req.ID)
|
||||
case "compose":
|
||||
filePath := ""
|
||||
cli, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer cli.Close()
|
||||
config, workdir := "", ""
|
||||
options := container.ListOptions{All: true}
|
||||
options.Filters = filters.NewArgs()
|
||||
options.Filters.Add("label", fmt.Sprintf("%s=%s", composeProjectLabel, req.ID))
|
||||
containers, err := cli.ContainerList(context.Background(), options)
|
||||
containers, err := client.ContainerList(context.Background(), options)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, container := range containers {
|
||||
config := container.Labels[composeConfigLabel]
|
||||
if len(req.Detail) != 0 && strings.Contains(config, req.Detail) {
|
||||
config = req.Detail
|
||||
}
|
||||
workdir := container.Labels[composeWorkdirLabel]
|
||||
if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) {
|
||||
filePath = config
|
||||
break
|
||||
} else {
|
||||
filePath = workdir
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(containers) == 0 {
|
||||
composeItem, _ := composeRepo.GetRecord(repo.WithByName(req.ID))
|
||||
filePath = composeItem.Path
|
||||
composeItem, err := composeRepo.GetRecord(repo.WithByName(req.ID))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
config = composeItem.Path
|
||||
workdir = filepath.Dir(primaryComposePath(config))
|
||||
} else {
|
||||
config = containers[0].Labels[composeConfigLabel]
|
||||
workdir = containers[0].Labels[composeWorkdirLabel]
|
||||
}
|
||||
if _, err := os.Stat(filePath); err != nil {
|
||||
composeFiles, err := resolveComposeFiles(workdir, config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
filePath := composeFiles[0]
|
||||
if strings.TrimSpace(req.Detail) != "" {
|
||||
selectedFiles, err := resolveComposeFiles(workdir, req.Detail)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(selectedFiles) != 1 || !slices.Contains(composeFiles, selectedFiles[0]) {
|
||||
return "", fmt.Errorf("compose file %s does not belong to project %s", req.Detail, req.ID)
|
||||
}
|
||||
filePath = selectedFiles[0]
|
||||
}
|
||||
info, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return "", fmt.Errorf("compose path %s is not a regular file", filePath)
|
||||
}
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -71,10 +71,9 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
||||
ContainerCount: 0,
|
||||
CreatedAt: localItem.CreatedAt.Format(constant.DateTimeLayout),
|
||||
ConfigFile: localItem.Path,
|
||||
Workdir: strings.TrimSuffix(localItem.Path, "/docker-compose.yml"),
|
||||
Workdir: filepath.Dir(primaryComposePath(localItem.Path)),
|
||||
}
|
||||
composeItemLocal.CreatedBy = "1Panel"
|
||||
composeItemLocal.Path = localItem.Path
|
||||
composeLocalMap[localItem.Name] = composeItemLocal
|
||||
}
|
||||
|
||||
@@ -112,11 +111,6 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
||||
if ok {
|
||||
composeItem.CreatedBy = createdBy
|
||||
}
|
||||
if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) {
|
||||
composeItem.Path = config
|
||||
} else {
|
||||
composeItem.Path = workdir
|
||||
}
|
||||
for i := 0; i < len(composeCreatedByLocal); i++ {
|
||||
if composeCreatedByLocal[i].Name == name {
|
||||
composeItem.CreatedBy = "1Panel"
|
||||
@@ -147,7 +141,9 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
||||
|
||||
for key, value := range mergedMap {
|
||||
value.Name = key
|
||||
value.ComposeFileExists = composeFileExists(value.Workdir, value.ConfigFile)
|
||||
composeFiles, err := resolveComposeFiles(value.Workdir, value.ConfigFile)
|
||||
value.Path = strings.Join(composeFiles, ",")
|
||||
value.ComposeFileExists = err == nil && composeFileExists(composeFiles)
|
||||
value.IsPinned = pinnedByName[key]
|
||||
records = append(records, value)
|
||||
}
|
||||
@@ -192,29 +188,6 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
||||
return int64(total), listItem, nil
|
||||
}
|
||||
|
||||
func composeFileExists(workdir, configFile string) bool {
|
||||
workdir = strings.TrimSpace(workdir)
|
||||
configFile = strings.TrimSpace(configFile)
|
||||
if configFile == "" {
|
||||
return false
|
||||
}
|
||||
for _, item := range strings.Split(configFile, ",") {
|
||||
file := strings.TrimSpace(item)
|
||||
if file == "" {
|
||||
continue
|
||||
}
|
||||
if !filepath.IsAbs(file) && workdir != "" {
|
||||
file = filepath.Join(workdir, file)
|
||||
}
|
||||
file = filepath.Clean(file)
|
||||
info, err := os.Stat(file)
|
||||
if err == nil && !info.IsDir() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
|
||||
if err := validateComposeCreateName(req); err != nil {
|
||||
return false, err
|
||||
@@ -522,10 +495,6 @@ func isComposeProjectNameEmptyError(err error) bool {
|
||||
}
|
||||
|
||||
func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
|
||||
if len(req.Path) == 0 && req.Operation == "delete" {
|
||||
_ = composeRepo.DeleteRecord(repo.WithByName(req.Name))
|
||||
return nil
|
||||
}
|
||||
if cmd.CheckIllegal(req.Path, req.Operation) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
@@ -535,13 +504,19 @@ func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
|
||||
}
|
||||
if req.WithFile {
|
||||
for _, item := range strings.Split(req.Path, ",") {
|
||||
if len(item) != 0 {
|
||||
_ = os.RemoveAll(path.Dir(item))
|
||||
item = strings.TrimSpace(item)
|
||||
if !filepath.IsAbs(item) || filepath.Dir(item) == "/" {
|
||||
continue
|
||||
}
|
||||
info, err := os.Stat(item)
|
||||
if err == nil && info.Mode().IsRegular() {
|
||||
if err := os.RemoveAll(filepath.Dir(item)); err != nil && !req.Force {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = composeRepo.DeleteRecord(repo.WithByName(req.Name))
|
||||
return nil
|
||||
return composeRepo.DeleteRecord(repo.WithByName(req.Name))
|
||||
}
|
||||
if req.Operation == "up" {
|
||||
if stdout, err := compose.Up(req.Path, req.Name); err != nil {
|
||||
@@ -584,13 +559,13 @@ func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error {
|
||||
write.Flush()
|
||||
|
||||
global.LOG.Infof("docker-compose.yml %s has been replaced, now start to docker-compose restart", req.DetailPath)
|
||||
if err := newComposeEnv(req.DetailPath, req.Env); err != nil {
|
||||
if err := newComposeEnv(primaryComposePath(req.Path), req.Env); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := compose.UpWithTask(req.Path, t, req.ForcePull, req.Name); err != nil {
|
||||
global.LOG.Errorf("update failed when handle compose up, err: %s, now try to recreate the old compose file", err)
|
||||
if err := recreateCompose(string(oldFile), req.Path, req.Name); err != nil {
|
||||
if err := recreateCompose(string(oldFile), req.Path, req.DetailPath, req.Name); err != nil {
|
||||
return fmt.Errorf("update failed and recreate old compose file also failed, err: %v", err)
|
||||
}
|
||||
return fmt.Errorf("update failed when handle compose up, err: %s", err)
|
||||
@@ -702,9 +677,18 @@ func (u *ContainerService) loadPath(req *dto.ComposeCreate) error {
|
||||
}
|
||||
|
||||
func removeContainerForCompose(composeName, composePath string) error {
|
||||
if _, err := os.Stat(composePath); err == nil {
|
||||
if stdout, err := compose.Operate(composePath, "down", composeName); err != nil {
|
||||
return errors.New(stdout)
|
||||
composeFiles, err := resolveComposeFiles("", composePath)
|
||||
filesAvailable := err == nil
|
||||
for _, file := range composeFiles {
|
||||
info, err := os.Stat(file)
|
||||
if err != nil || !info.Mode().IsRegular() {
|
||||
filesAvailable = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if filesAvailable {
|
||||
if stdout, err := compose.Operate(strings.Join(composeFiles, ","), "down", composeName); err != nil {
|
||||
return fmt.Errorf("docker-compose down failed, std: %s, err: %w", stdout, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -722,14 +706,17 @@ func removeContainerForCompose(composeName, composePath string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var removeErrors []error
|
||||
for _, c := range containers {
|
||||
_ = client.ContainerRemove(ctx, c.ID, container.RemoveOptions{RemoveVolumes: true, Force: true})
|
||||
if err := client.ContainerRemove(ctx, c.ID, container.RemoveOptions{RemoveVolumes: true, Force: true}); err != nil {
|
||||
removeErrors = append(removeErrors, fmt.Errorf("remove container %s failed: %w", c.ID, err))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return errors.Join(removeErrors...)
|
||||
}
|
||||
|
||||
func recreateCompose(content, path, projectName string) error {
|
||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0640)
|
||||
func recreateCompose(content, composePath, detailPath, projectName string) error {
|
||||
file, err := os.OpenFile(detailPath, os.O_WRONLY|os.O_TRUNC, 0640)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -738,7 +725,7 @@ func recreateCompose(content, path, projectName string) error {
|
||||
_, _ = write.WriteString(content)
|
||||
write.Flush()
|
||||
|
||||
if stdout, err := compose.Up(path, projectName); err != nil {
|
||||
if stdout, err := compose.Up(composePath, projectName); err != nil {
|
||||
return errors.New(string(stdout))
|
||||
}
|
||||
return nil
|
||||
@@ -746,6 +733,9 @@ func recreateCompose(content, path, projectName string) error {
|
||||
|
||||
func loadEnv(list []dto.ComposeInfo) []dto.ComposeInfo {
|
||||
for i := 0; i < len(list); i++ {
|
||||
if list[i].Path == "" {
|
||||
continue
|
||||
}
|
||||
tmpPath := list[i].Path
|
||||
if strings.Contains(list[i].Path, ",") {
|
||||
tmpPath = strings.Split(list[i].Path, ",")[0]
|
||||
@@ -774,3 +764,35 @@ func newComposeEnv(pathItem string, env string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveComposeFiles(workdir, configFile string) ([]string, error) {
|
||||
workdir = strings.TrimSpace(workdir)
|
||||
var files []string
|
||||
for _, item := range strings.Split(configFile, ",") {
|
||||
file := strings.TrimSpace(item)
|
||||
if file == "" {
|
||||
continue
|
||||
}
|
||||
if !filepath.IsAbs(file) {
|
||||
if !filepath.IsAbs(workdir) {
|
||||
return nil, fmt.Errorf("cannot resolve compose file %s without an absolute working directory", file)
|
||||
}
|
||||
file = filepath.Join(workdir, file)
|
||||
}
|
||||
files = append(files, filepath.Clean(file))
|
||||
}
|
||||
if len(files) == 0 {
|
||||
return nil, fmt.Errorf("compose config file is not specified")
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func composeFileExists(files []string) bool {
|
||||
for _, file := range files {
|
||||
info, err := os.Stat(file)
|
||||
if err == nil && info.Mode().IsRegular() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -350,6 +350,7 @@
|
||||
v-if="showType !== 'log' && !(showType === 'env' && isAppStoreCompose)"
|
||||
v-permission
|
||||
type="primary"
|
||||
:disabled="detailLoading || !composeContent"
|
||||
@click="onSubmitEdit"
|
||||
>
|
||||
{{ $t('commons.button.save') }}
|
||||
@@ -741,18 +742,24 @@ const loadDetail = async (row: Container.ComposeInfo, withRefresh: boolean) => {
|
||||
};
|
||||
|
||||
const inspectCompose = async (name: string, detailPath: string) => {
|
||||
composeContent.value = '';
|
||||
if (!currentCompose.value?.composeFileExists) {
|
||||
composeContent.value = '';
|
||||
loadContainerStats();
|
||||
detailLoading.value = false;
|
||||
return;
|
||||
}
|
||||
detailLoading.value = true;
|
||||
await inspect({ id: name, type: 'compose', detail: detailPath })
|
||||
.then((res) => {
|
||||
if (currentCompose.value?.name !== name || currentYamlPath.value !== detailPath) {
|
||||
return;
|
||||
}
|
||||
composeContent.value = res.data;
|
||||
detailLoading.value = false;
|
||||
})
|
||||
.finally(() => {
|
||||
if (currentCompose.value?.name !== name || currentYamlPath.value !== detailPath) {
|
||||
return;
|
||||
}
|
||||
loadContainerStats();
|
||||
detailLoading.value = false;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user