2022-10-18 10:39:45 +00:00
|
|
|
package service
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
2022-12-06 07:05:13 +00:00
|
|
|
"errors"
|
2022-10-18 10:39:45 +00:00
|
|
|
"fmt"
|
2023-04-26 09:00:20 +00:00
|
|
|
"io"
|
2022-10-18 10:39:45 +00:00
|
|
|
"os"
|
2023-03-20 10:16:26 +00:00
|
|
|
"os/exec"
|
2023-03-17 11:11:03 +00:00
|
|
|
"path"
|
2024-09-27 14:25:37 +00:00
|
|
|
"path/filepath"
|
2022-12-06 10:24:26 +00:00
|
|
|
"sort"
|
2022-10-18 10:39:45 +00:00
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
2024-04-25 08:45:12 +00:00
|
|
|
"github.com/docker/docker/api/types/container"
|
|
|
|
|
2022-10-18 10:39:45 +00:00
|
|
|
"github.com/1Panel-dev/1Panel/backend/app/dto"
|
2022-12-06 07:05:13 +00:00
|
|
|
"github.com/1Panel-dev/1Panel/backend/app/model"
|
2023-07-17 08:34:29 +00:00
|
|
|
"github.com/1Panel-dev/1Panel/backend/buserr"
|
2022-10-18 10:39:45 +00:00
|
|
|
"github.com/1Panel-dev/1Panel/backend/constant"
|
|
|
|
"github.com/1Panel-dev/1Panel/backend/global"
|
2023-07-17 08:34:29 +00:00
|
|
|
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
|
2022-12-06 07:05:13 +00:00
|
|
|
"github.com/1Panel-dev/1Panel/backend/utils/compose"
|
2022-10-18 10:39:45 +00:00
|
|
|
"github.com/1Panel-dev/1Panel/backend/utils/docker"
|
|
|
|
"github.com/docker/docker/api/types/filters"
|
|
|
|
"golang.org/x/net/context"
|
|
|
|
)
|
|
|
|
|
|
|
|
const composeProjectLabel = "com.docker.compose.project"
|
|
|
|
const composeConfigLabel = "com.docker.compose.project.config_files"
|
|
|
|
const composeWorkdirLabel = "com.docker.compose.project.working_dir"
|
|
|
|
const composeCreatedBy = "createdBy"
|
|
|
|
|
2024-09-18 06:00:49 +00:00
|
|
|
type DockerCompose struct {
|
|
|
|
Version string `yaml:"version"`
|
|
|
|
Services map[string]map[string]interface{} `yaml:"services"`
|
|
|
|
Networks map[string]interface{} `yaml:"networks"`
|
|
|
|
}
|
|
|
|
|
2023-02-07 10:48:32 +00:00
|
|
|
func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface{}, error) {
|
2022-10-18 10:39:45 +00:00
|
|
|
var (
|
|
|
|
records []dto.ComposeInfo
|
|
|
|
BackDatas []dto.ComposeInfo
|
|
|
|
)
|
|
|
|
client, err := docker.NewDockerClient()
|
|
|
|
if err != nil {
|
|
|
|
return 0, nil, err
|
|
|
|
}
|
2024-04-25 08:45:12 +00:00
|
|
|
defer client.Close()
|
2022-10-18 10:39:45 +00:00
|
|
|
|
2024-04-08 09:54:07 +00:00
|
|
|
options := container.ListOptions{All: true}
|
2022-10-18 10:39:45 +00:00
|
|
|
options.Filters = filters.NewArgs()
|
|
|
|
options.Filters.Add("label", composeProjectLabel)
|
|
|
|
|
|
|
|
list, err := client.ContainerList(context.Background(), options)
|
|
|
|
if err != nil {
|
|
|
|
return 0, nil, err
|
|
|
|
}
|
2022-12-06 07:05:13 +00:00
|
|
|
|
|
|
|
composeCreatedByLocal, _ := composeRepo.ListRecord()
|
2024-09-12 10:06:22 +00:00
|
|
|
|
|
|
|
composeLocalMap := make(map[string]dto.ComposeInfo)
|
|
|
|
for _, localItem := range composeCreatedByLocal {
|
|
|
|
composeItemLocal := dto.ComposeInfo{
|
|
|
|
ContainerNumber: 0,
|
|
|
|
CreatedAt: localItem.CreatedAt.Format(constant.DateTimeLayout),
|
|
|
|
ConfigFile: localItem.Path,
|
|
|
|
Workdir: strings.TrimSuffix(localItem.Path, "/docker-compose.yml"),
|
|
|
|
}
|
|
|
|
composeItemLocal.CreatedBy = "1Panel"
|
|
|
|
composeItemLocal.Path = localItem.Path
|
|
|
|
composeLocalMap[localItem.Name] = composeItemLocal
|
|
|
|
}
|
|
|
|
|
2022-10-18 10:39:45 +00:00
|
|
|
composeMap := make(map[string]dto.ComposeInfo)
|
|
|
|
for _, container := range list {
|
|
|
|
if name, ok := container.Labels[composeProjectLabel]; ok {
|
|
|
|
containerItem := dto.ComposeContainer{
|
|
|
|
ContainerID: container.ID,
|
|
|
|
Name: container.Names[0][1:],
|
|
|
|
State: container.State,
|
2024-06-28 06:04:08 +00:00
|
|
|
CreateTime: time.Unix(container.Created, 0).Format(constant.DateTimeLayout),
|
2022-10-18 10:39:45 +00:00
|
|
|
}
|
|
|
|
if compose, has := composeMap[name]; has {
|
|
|
|
compose.ContainerNumber++
|
|
|
|
compose.Containers = append(compose.Containers, containerItem)
|
|
|
|
composeMap[name] = compose
|
|
|
|
} else {
|
|
|
|
config := container.Labels[composeConfigLabel]
|
|
|
|
workdir := container.Labels[composeWorkdirLabel]
|
|
|
|
composeItem := dto.ComposeInfo{
|
|
|
|
ContainerNumber: 1,
|
2024-06-28 06:04:08 +00:00
|
|
|
CreatedAt: time.Unix(container.Created, 0).Format(constant.DateTimeLayout),
|
2022-10-18 10:39:45 +00:00
|
|
|
ConfigFile: config,
|
|
|
|
Workdir: workdir,
|
|
|
|
Containers: []dto.ComposeContainer{containerItem},
|
|
|
|
}
|
|
|
|
createdBy, ok := container.Labels[composeCreatedBy]
|
|
|
|
if ok {
|
|
|
|
composeItem.CreatedBy = createdBy
|
|
|
|
}
|
|
|
|
if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) {
|
|
|
|
composeItem.Path = config
|
|
|
|
} else {
|
|
|
|
composeItem.Path = workdir
|
|
|
|
}
|
2022-12-06 07:05:13 +00:00
|
|
|
for i := 0; i < len(composeCreatedByLocal); i++ {
|
|
|
|
if composeCreatedByLocal[i].Name == name {
|
2022-12-19 04:50:58 +00:00
|
|
|
composeItem.CreatedBy = "1Panel"
|
2022-12-06 07:05:13 +00:00
|
|
|
composeCreatedByLocal = append(composeCreatedByLocal[:i], composeCreatedByLocal[i+1:]...)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2022-10-18 10:39:45 +00:00
|
|
|
composeMap[name] = composeItem
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-09-12 10:06:22 +00:00
|
|
|
|
|
|
|
mergedMap := make(map[string]dto.ComposeInfo)
|
|
|
|
for key, localItem := range composeLocalMap {
|
|
|
|
mergedMap[key] = localItem
|
|
|
|
}
|
|
|
|
for key, item := range composeMap {
|
|
|
|
if existingItem, exists := mergedMap[key]; exists {
|
|
|
|
if item.ContainerNumber > 0 {
|
|
|
|
if existingItem.ContainerNumber <= 0 {
|
|
|
|
mergedMap[key] = item
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
mergedMap[key] = item
|
2022-12-06 07:05:13 +00:00
|
|
|
}
|
|
|
|
}
|
2024-09-12 10:06:22 +00:00
|
|
|
|
|
|
|
for key, value := range mergedMap {
|
2022-10-18 10:39:45 +00:00
|
|
|
value.Name = key
|
|
|
|
records = append(records, value)
|
|
|
|
}
|
2023-02-07 10:48:32 +00:00
|
|
|
if len(req.Info) != 0 {
|
2023-05-30 07:30:57 +00:00
|
|
|
length, count := len(records), 0
|
|
|
|
for count < length {
|
2023-02-07 10:48:32 +00:00
|
|
|
if !strings.Contains(records[count].Name, req.Info) {
|
|
|
|
records = append(records[:count], records[(count+1):]...)
|
2023-05-30 07:30:57 +00:00
|
|
|
length--
|
2023-02-07 10:48:32 +00:00
|
|
|
} else {
|
|
|
|
count++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-12-06 10:24:26 +00:00
|
|
|
sort.Slice(records, func(i, j int) bool {
|
|
|
|
return records[i].CreatedAt > records[j].CreatedAt
|
|
|
|
})
|
2022-10-18 10:39:45 +00:00
|
|
|
total, start, end := len(records), (req.Page-1)*req.PageSize, req.Page*req.PageSize
|
|
|
|
if start > total {
|
|
|
|
BackDatas = make([]dto.ComposeInfo, 0)
|
|
|
|
} else {
|
|
|
|
if end >= total {
|
|
|
|
end = total
|
|
|
|
}
|
|
|
|
BackDatas = records[start:end]
|
|
|
|
}
|
2024-09-27 14:25:37 +00:00
|
|
|
listItem := loadEnv(BackDatas)
|
|
|
|
return int64(total), listItem, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func loadEnv(list []dto.ComposeInfo) []dto.ComposeInfo {
|
|
|
|
for i := 0; i < len(list); i++ {
|
|
|
|
envFilePath := filepath.Join(path.Dir(list[i].Path), "1panel.env")
|
|
|
|
file, err := os.ReadFile(envFilePath)
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
lines := strings.Split(string(file), "\n")
|
|
|
|
for _, line := range lines {
|
|
|
|
lineItem := strings.TrimSpace(line)
|
|
|
|
if len(lineItem) != 0 && !strings.HasPrefix(lineItem, "#") {
|
|
|
|
list[i].Env = append(list[i].Env, lineItem)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return list
|
2022-10-18 10:39:45 +00:00
|
|
|
}
|
|
|
|
|
2023-03-21 10:42:37 +00:00
|
|
|
func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
|
2023-07-17 08:34:29 +00:00
|
|
|
if cmd.CheckIllegal(req.Path) {
|
|
|
|
return false, buserr.New(constant.ErrCmdIllegal)
|
|
|
|
}
|
2023-07-06 06:26:22 +00:00
|
|
|
composeItem, _ := composeRepo.GetRecord(commonRepo.WithByName(req.Name))
|
|
|
|
if composeItem.ID != 0 {
|
|
|
|
return false, constant.ErrRecordExist
|
|
|
|
}
|
2023-03-21 10:42:37 +00:00
|
|
|
if err := u.loadPath(&req); err != nil {
|
|
|
|
return false, err
|
2022-10-18 10:39:45 +00:00
|
|
|
}
|
2024-09-27 14:25:37 +00:00
|
|
|
if err := newComposeEnv(req.Path, req.Env); err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
2023-03-21 10:42:37 +00:00
|
|
|
cmd := exec.Command("docker-compose", "-f", req.Path, "config")
|
|
|
|
stdout, err := cmd.CombinedOutput()
|
|
|
|
if err != nil {
|
|
|
|
return false, errors.New(string(stdout))
|
|
|
|
}
|
|
|
|
return true, nil
|
|
|
|
}
|
2022-10-18 10:39:45 +00:00
|
|
|
|
2023-03-21 10:42:37 +00:00
|
|
|
func (u *ContainerService) CreateCompose(req dto.ComposeCreate) (string, error) {
|
2023-07-17 08:34:29 +00:00
|
|
|
if cmd.CheckIllegal(req.Name, req.Path) {
|
|
|
|
return "", buserr.New(constant.ErrCmdIllegal)
|
|
|
|
}
|
2023-03-21 10:42:37 +00:00
|
|
|
if err := u.loadPath(&req); err != nil {
|
|
|
|
return "", err
|
2022-10-18 10:39:45 +00:00
|
|
|
}
|
2022-12-28 10:35:53 +00:00
|
|
|
global.LOG.Infof("docker-compose.yml %s create successful, start to docker-compose up", req.Name)
|
2023-03-17 11:11:03 +00:00
|
|
|
|
|
|
|
if req.From == "path" {
|
2023-08-02 08:47:30 +00:00
|
|
|
req.Name = path.Base(path.Dir(req.Path))
|
2023-03-17 11:11:03 +00:00
|
|
|
}
|
2023-08-16 15:34:15 +00:00
|
|
|
|
|
|
|
dockerLogDir := path.Join(global.CONF.System.TmpDir, "docker_logs")
|
2023-08-17 08:10:09 +00:00
|
|
|
if _, err := os.Stat(dockerLogDir); err != nil && os.IsNotExist(err) {
|
|
|
|
if err = os.MkdirAll(dockerLogDir, os.ModePerm); err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
}
|
2024-06-28 06:04:08 +00:00
|
|
|
logItem := fmt.Sprintf("%s/compose_create_%s_%s.log", dockerLogDir, req.Name, time.Now().Format(constant.DateTimeSlimLayout))
|
2023-08-16 15:34:15 +00:00
|
|
|
file, err := os.OpenFile(logItem, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
|
2023-03-20 10:16:26 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
2022-12-02 10:52:43 +00:00
|
|
|
}
|
2024-09-27 14:25:37 +00:00
|
|
|
if err := newComposeEnv(req.Path, req.Env); err != nil {
|
|
|
|
return "", err
|
2024-09-18 06:00:49 +00:00
|
|
|
}
|
2023-03-20 10:16:26 +00:00
|
|
|
go func() {
|
|
|
|
defer file.Close()
|
|
|
|
cmd := exec.Command("docker-compose", "-f", req.Path, "up", "-d")
|
2023-04-26 09:00:20 +00:00
|
|
|
multiWriter := io.MultiWriter(os.Stdout, file)
|
|
|
|
cmd.Stdout = multiWriter
|
|
|
|
cmd.Stderr = multiWriter
|
|
|
|
if err := cmd.Run(); err != nil {
|
2023-03-20 10:16:26 +00:00
|
|
|
global.LOG.Errorf("docker-compose up %s failed, err: %v", req.Name, err)
|
|
|
|
_, _ = compose.Down(req.Path)
|
|
|
|
_, _ = file.WriteString("docker-compose up failed!")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
global.LOG.Infof("docker-compose up %s successful!", req.Name)
|
2024-09-12 10:06:22 +00:00
|
|
|
_ = composeRepo.CreateRecord(&model.Compose{Name: req.Name, Path: req.Path})
|
2023-03-20 10:16:26 +00:00
|
|
|
_, _ = file.WriteString("docker-compose up successful!")
|
|
|
|
}()
|
2023-03-17 11:11:03 +00:00
|
|
|
|
2023-08-16 15:34:15 +00:00
|
|
|
return path.Base(logItem), nil
|
2022-10-18 10:39:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
|
2023-07-17 08:34:29 +00:00
|
|
|
if cmd.CheckIllegal(req.Path, req.Operation) {
|
|
|
|
return buserr.New(constant.ErrCmdIllegal)
|
|
|
|
}
|
2022-12-06 07:05:13 +00:00
|
|
|
if _, err := os.Stat(req.Path); err != nil {
|
|
|
|
return fmt.Errorf("load file with path %s failed, %v", req.Path, err)
|
|
|
|
}
|
2024-09-12 10:06:22 +00:00
|
|
|
if req.Operation == "up" {
|
|
|
|
if stdout, err := compose.Up(req.Path); err != nil {
|
|
|
|
return errors.New(string(stdout))
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if stdout, err := compose.Operate(req.Path, req.Operation); err != nil {
|
|
|
|
return errors.New(string(stdout))
|
|
|
|
}
|
2022-12-06 07:05:13 +00:00
|
|
|
}
|
2022-12-28 10:35:53 +00:00
|
|
|
global.LOG.Infof("docker-compose %s %s successful", req.Operation, req.Name)
|
2022-12-06 07:05:13 +00:00
|
|
|
if req.Operation == "down" {
|
2023-04-27 04:44:18 +00:00
|
|
|
if req.WithFile {
|
2024-09-12 10:06:22 +00:00
|
|
|
_ = composeRepo.DeleteRecord(commonRepo.WithByName(req.Name))
|
2023-04-27 04:44:18 +00:00
|
|
|
_ = os.RemoveAll(path.Dir(req.Path))
|
2024-09-12 10:06:22 +00:00
|
|
|
} else {
|
|
|
|
composeItem, _ := composeRepo.GetRecord(commonRepo.WithByName(req.Name))
|
|
|
|
if composeItem.Path == "" {
|
|
|
|
upMap := make(map[string]interface{})
|
|
|
|
upMap["path"] = req.Path
|
|
|
|
_ = composeRepo.UpdateRecord(req.Name, upMap)
|
|
|
|
}
|
2023-04-27 04:44:18 +00:00
|
|
|
}
|
2022-12-06 07:05:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error {
|
2023-07-17 08:34:29 +00:00
|
|
|
if cmd.CheckIllegal(req.Name, req.Path) {
|
|
|
|
return buserr.New(constant.ErrCmdIllegal)
|
|
|
|
}
|
2023-12-12 07:34:08 +00:00
|
|
|
oldFile, err := os.ReadFile(req.Path)
|
|
|
|
if err != nil {
|
2022-12-06 07:05:13 +00:00
|
|
|
return fmt.Errorf("load file with path %s failed, %v", req.Path, err)
|
|
|
|
}
|
2024-09-27 14:25:37 +00:00
|
|
|
file, err := os.OpenFile(req.Path, os.O_WRONLY|os.O_TRUNC, 0640)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2022-10-18 10:39:45 +00:00
|
|
|
}
|
2024-09-27 14:25:37 +00:00
|
|
|
defer file.Close()
|
|
|
|
write := bufio.NewWriter(file)
|
|
|
|
_, _ = write.WriteString(req.Content)
|
|
|
|
write.Flush()
|
|
|
|
|
|
|
|
global.LOG.Infof("docker-compose.yml %s has been replaced, now start to docker-compose restart", req.Path)
|
|
|
|
if err := newComposeEnv(req.Path, req.Env); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-12-06 07:05:13 +00:00
|
|
|
if stdout, err := compose.Up(req.Path); err != nil {
|
2023-12-12 07:34:08 +00:00
|
|
|
if err := recreateCompose(string(oldFile), req.Path); err != nil {
|
|
|
|
return fmt.Errorf("update failed when handle compose up, err: %s, recreate failed: %v", string(stdout), err)
|
|
|
|
}
|
|
|
|
return fmt.Errorf("update failed when handle compose up, err: %s", string(stdout))
|
2022-12-06 07:05:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2022-10-18 10:39:45 +00:00
|
|
|
}
|
2023-03-21 10:42:37 +00:00
|
|
|
|
|
|
|
func (u *ContainerService) loadPath(req *dto.ComposeCreate) error {
|
2023-04-27 02:24:14 +00:00
|
|
|
if req.From == "template" || req.From == "edit" {
|
2023-03-21 10:42:37 +00:00
|
|
|
dir := fmt.Sprintf("%s/docker/compose/%s", constant.DataDir, req.Name)
|
|
|
|
if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
|
|
|
|
if err = os.MkdirAll(dir, os.ModePerm); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
path := fmt.Sprintf("%s/docker-compose.yml", dir)
|
|
|
|
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
write := bufio.NewWriter(file)
|
|
|
|
_, _ = write.WriteString(string(req.File))
|
|
|
|
write.Flush()
|
|
|
|
req.Path = path
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2023-12-12 07:34:08 +00:00
|
|
|
|
|
|
|
func recreateCompose(content, path string) error {
|
|
|
|
file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0640)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
write := bufio.NewWriter(file)
|
|
|
|
_, _ = write.WriteString(content)
|
|
|
|
write.Flush()
|
|
|
|
|
|
|
|
if stdout, err := compose.Up(path); err != nil {
|
|
|
|
return errors.New(string(stdout))
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2024-09-27 14:25:37 +00:00
|
|
|
|
|
|
|
func newComposeEnv(pathItem string, env []string) error {
|
|
|
|
if len(env) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
envFilePath := path.Join(path.Dir(pathItem), "1panel.env")
|
|
|
|
|
|
|
|
file, err := os.OpenFile(envFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
|
|
|
|
if err != nil {
|
|
|
|
global.LOG.Errorf("failed to create env file: %v", err)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
for _, env := range env {
|
|
|
|
envItem := strings.TrimSpace(env)
|
|
|
|
if _, err := file.WriteString(fmt.Sprintf("%s\n", envItem)); err != nil {
|
|
|
|
global.LOG.Errorf("failed to write env to file: %v", err)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
global.LOG.Infof("1panel.env file successfully created or updated with env variables in %s", envFilePath)
|
|
|
|
return nil
|
|
|
|
}
|