mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/1Panel-dev/1Panel.git
synced 2026-09-20 16:13:59 +08:00
777 lines
22 KiB
Go
777 lines
22 KiB
Go
package service
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
|
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
|
"github.com/1Panel-dev/1Panel/agent/global"
|
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/re"
|
|
"github.com/docker/docker/api/types/container"
|
|
"github.com/docker/docker/api/types/filters"
|
|
"gopkg.in/yaml.v3"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
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"
|
|
|
|
func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface{}, error) {
|
|
var (
|
|
records []dto.ComposeInfo
|
|
BackDatas []dto.ComposeInfo
|
|
)
|
|
client, err := docker.NewDockerClient()
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
defer client.Close()
|
|
|
|
options := container.ListOptions{All: true}
|
|
options.Filters = filters.NewArgs()
|
|
options.Filters.Add("label", composeProjectLabel)
|
|
|
|
list, err := client.ContainerList(context.Background(), options)
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
|
|
composeRecords, _ := composeRepo.ListRecord()
|
|
pinnedByName := make(map[string]bool, len(composeRecords))
|
|
composeCreatedByLocal := make([]model.Compose, 0, len(composeRecords))
|
|
for _, record := range composeRecords {
|
|
pinnedByName[record.Name] = record.IsPinned
|
|
if len(record.Path) != 0 {
|
|
composeCreatedByLocal = append(composeCreatedByLocal, record)
|
|
}
|
|
}
|
|
composeLocalMap := make(map[string]dto.ComposeInfo)
|
|
for _, localItem := range composeCreatedByLocal {
|
|
composeItemLocal := dto.ComposeInfo{
|
|
ContainerCount: 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
|
|
}
|
|
|
|
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,
|
|
CreateTime: time.Unix(container.Created, 0).Format(constant.DateTimeLayout),
|
|
Ports: transPortToStr(container.Ports),
|
|
}
|
|
if compose, has := composeMap[name]; has {
|
|
compose.ContainerCount++
|
|
if strings.ToLower(containerItem.State) == "running" {
|
|
compose.RunningCount++
|
|
}
|
|
compose.Containers = append(compose.Containers, containerItem)
|
|
composeMap[name] = compose
|
|
} else {
|
|
config := container.Labels[composeConfigLabel]
|
|
workdir := container.Labels[composeWorkdirLabel]
|
|
composeItem := dto.ComposeInfo{
|
|
ContainerCount: 1,
|
|
CreatedAt: time.Unix(container.Created, 0).Format(constant.DateTimeLayout),
|
|
ConfigFile: config,
|
|
Workdir: workdir,
|
|
Containers: []dto.ComposeContainer{containerItem},
|
|
}
|
|
if strings.ToLower(containerItem.State) == "running" {
|
|
composeItem.RunningCount = 1
|
|
}
|
|
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
|
|
}
|
|
for i := 0; i < len(composeCreatedByLocal); i++ {
|
|
if composeCreatedByLocal[i].Name == name {
|
|
composeItem.CreatedBy = "1Panel"
|
|
composeCreatedByLocal = append(composeCreatedByLocal[:i], composeCreatedByLocal[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
composeMap[name] = composeItem
|
|
}
|
|
}
|
|
}
|
|
|
|
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.ContainerCount > 0 {
|
|
if existingItem.ContainerCount <= 0 {
|
|
mergedMap[key] = item
|
|
}
|
|
}
|
|
} else {
|
|
mergedMap[key] = item
|
|
}
|
|
}
|
|
|
|
for key, value := range mergedMap {
|
|
value.Name = key
|
|
value.ComposeFileExists = composeFileExists(value.Workdir, value.ConfigFile)
|
|
value.IsPinned = pinnedByName[key]
|
|
records = append(records, value)
|
|
}
|
|
if len(req.Info) != 0 {
|
|
length, count := len(records), 0
|
|
for count < length {
|
|
if !strings.Contains(records[count].Name, req.Info) {
|
|
records = append(records[:count], records[(count+1):]...)
|
|
length--
|
|
} else {
|
|
count++
|
|
}
|
|
}
|
|
}
|
|
if req.ExcludeAppStore {
|
|
length, count := len(records), 0
|
|
for count < length {
|
|
if records[count].CreatedBy == "Apps" {
|
|
records = append(records[:count], records[(count+1):]...)
|
|
length--
|
|
} else {
|
|
count++
|
|
}
|
|
}
|
|
}
|
|
sort.Slice(records, func(i, j int) bool {
|
|
if records[i].IsPinned != records[j].IsPinned {
|
|
return records[i].IsPinned
|
|
}
|
|
return records[i].CreatedAt > records[j].CreatedAt
|
|
})
|
|
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]
|
|
}
|
|
listItem := loadEnv(BackDatas)
|
|
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
|
|
}
|
|
if hasIllegalComposeCreateInput(req) {
|
|
return false, buserr.New("ErrCmdIllegal")
|
|
}
|
|
projectName, err := resolveComposeCreateProjectName(req)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if err := checkComposeCreateDuplicate(req, projectName); err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error {
|
|
if err := validateComposeCreateName(req); err != nil {
|
|
return err
|
|
}
|
|
if hasIllegalComposeCreateInput(req) {
|
|
return buserr.New("ErrCmdIllegal")
|
|
}
|
|
projectName, err := resolveComposeCreateProjectName(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := checkComposeCreateDuplicate(req, projectName); err != nil {
|
|
return err
|
|
}
|
|
if err := u.loadPath(&req); err != nil {
|
|
return err
|
|
}
|
|
if err := newComposeEnv(req.Path, req.Env); err != nil {
|
|
return err
|
|
}
|
|
req.Name = projectName
|
|
recordName := strings.ToLower(req.Name)
|
|
if err := saveComposeRecord(recordName, req.Path); err != nil {
|
|
return fmt.Errorf("save compose record failed, err: %v", err)
|
|
}
|
|
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskCreate, task.TaskScopeCompose, req.TaskID, 1)
|
|
if err != nil {
|
|
return fmt.Errorf("new task for image build failed, err: %v", err)
|
|
}
|
|
go func() {
|
|
taskItem.AddSubTask(i18n.GetMsgByKey("ComposeCreate"), func(t *task.Task) error {
|
|
err := compose.UpWithTask(req.Path, t, req.ForcePull, req.Name)
|
|
t.LogWithStatus(i18n.GetMsgByKey("ComposeCreate"), err)
|
|
return err
|
|
}, nil)
|
|
_ = taskItem.Execute()
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
func saveComposeRecord(name, composePath string) error {
|
|
record, err := composeRepo.GetRecord(repo.WithByName(name))
|
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return err
|
|
}
|
|
if record.ID == 0 {
|
|
return composeRepo.CreateRecord(&model.Compose{Name: name, Path: composePath})
|
|
}
|
|
return composeRepo.UpdateRecord(name, map[string]interface{}{"path": composePath})
|
|
}
|
|
|
|
func checkComposeRecordName(name string) error {
|
|
composeItem, _ := composeRepo.GetRecord(repo.WithByName(name))
|
|
if composeItem.ID != 0 && len(composeItem.Path) != 0 {
|
|
return buserr.New("ErrRecordExist")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func checkComposeCreateDuplicate(req dto.ComposeCreate, projectName string) error {
|
|
if err := checkComposeRecordName(projectName); err != nil {
|
|
return err
|
|
}
|
|
if req.From == "path" {
|
|
return nil
|
|
}
|
|
composeItem, _ := composeRepo.GetRecord(repo.WithByPath(composeCreatePath(req)))
|
|
if composeItem.ID != 0 && composeItem.Path != "" {
|
|
return buserr.New("ErrRecordExist")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateComposeCreateName(req dto.ComposeCreate) error {
|
|
if req.From == "path" {
|
|
name := strings.TrimSpace(req.Name)
|
|
if name != "" && !re.GetRegex(re.ComposeNamePattern).MatchString(name) {
|
|
return buserr.New("ErrComposeNameInvalid")
|
|
}
|
|
return nil
|
|
}
|
|
if !re.GetRegex(re.ComposeNamePattern).MatchString(composeCreateDirName(req)) {
|
|
return buserr.New("ErrComposeNameInvalid")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func hasIllegalComposeCreateInput(req dto.ComposeCreate) bool {
|
|
if req.From == "path" {
|
|
return cmd.CheckIllegal(req.Name, req.Path)
|
|
}
|
|
return cmd.CheckIllegal(composeCreateDirName(req))
|
|
}
|
|
|
|
func composeCreateDirName(req dto.ComposeCreate) string {
|
|
dirName := strings.TrimSpace(req.DirName)
|
|
if dirName == "" {
|
|
// Keep compatibility with callers that used name as both the directory and
|
|
// Compose project name before dirName was introduced.
|
|
return strings.TrimSpace(req.Name)
|
|
}
|
|
return dirName
|
|
}
|
|
|
|
func composeCreatePath(req dto.ComposeCreate) string {
|
|
return filepath.Join(global.Dir.DataDir, "docker", "compose", composeCreateDirName(req), "docker-compose.yml")
|
|
}
|
|
|
|
func resolveComposeCreateProjectName(req dto.ComposeCreate) (string, error) {
|
|
if req.From == "path" {
|
|
envPath, err := createComposeTempFile(
|
|
filepath.Dir(primaryComposePath(req.Path)),
|
|
".1panel-compose-*.env",
|
|
req.Env,
|
|
)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer os.Remove(envPath)
|
|
return resolveComposeProjectName(req.Path, req.Name, envPath)
|
|
}
|
|
|
|
dir := filepath.Dir(composeCreatePath(req))
|
|
cleanupDir, err := prepareComposeStagingDir(dir)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer cleanupDir()
|
|
|
|
composePath, err := createComposeTempFile(dir, ".1panel-compose-*.yml", req.File)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer os.Remove(composePath)
|
|
|
|
envPath, err := createComposeTempFile(dir, ".1panel-compose-*.env", req.Env)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer os.Remove(envPath)
|
|
|
|
return resolveComposeProjectName(composePath, "", envPath)
|
|
}
|
|
|
|
func createComposeTempFile(dir, pattern, content string) (string, error) {
|
|
file, err := os.CreateTemp(dir, pattern)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
filePath := file.Name()
|
|
if _, err := file.WriteString(content); err != nil {
|
|
_ = file.Close()
|
|
_ = os.Remove(filePath)
|
|
return "", err
|
|
}
|
|
if err := file.Close(); err != nil {
|
|
_ = os.Remove(filePath)
|
|
return "", err
|
|
}
|
|
return filePath, nil
|
|
}
|
|
|
|
func prepareComposeStagingDir(dir string) (func(), error) {
|
|
if err := os.MkdirAll(filepath.Dir(dir), os.ModePerm); err != nil {
|
|
return nil, err
|
|
}
|
|
created := false
|
|
if err := os.Mkdir(dir, os.ModePerm); err != nil {
|
|
if !errors.Is(err, os.ErrExist) {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
created = true
|
|
}
|
|
return func() {
|
|
if created {
|
|
_ = os.Remove(dir)
|
|
}
|
|
}, nil
|
|
}
|
|
|
|
func resolveComposeProjectName(composePath, fallbackName, envFile string) (string, error) {
|
|
// Preserve the name resolved by Compose (including a top-level name) so the
|
|
// container label and the local record always use the same project identity.
|
|
parentName := normalizeComposeProjectName(path.Base(path.Dir(primaryComposePath(composePath))))
|
|
fallbackName = strings.TrimSpace(fallbackName)
|
|
stdout, err := runComposeConfig(composePath, "", envFile)
|
|
if err == nil {
|
|
projectName, parseErr := loadComposeProjectName(stdout)
|
|
if parseErr != nil {
|
|
return "", parseErr
|
|
}
|
|
if projectName != "" {
|
|
if !re.GetRegex(re.ComposeNamePattern).MatchString(projectName) {
|
|
return "", buserr.New("ErrComposeNameInvalid")
|
|
}
|
|
return projectName, nil
|
|
}
|
|
if parentName != "" {
|
|
return parentName, nil
|
|
}
|
|
if fallbackName != "" {
|
|
if _, fallbackErr := runComposeConfig(composePath, fallbackName, envFile); fallbackErr != nil {
|
|
return "", fallbackErr
|
|
}
|
|
return fallbackName, nil
|
|
}
|
|
return "", buserr.New("ErrComposeProjectNameEmpty")
|
|
}
|
|
if !isComposeProjectNameEmptyError(err) {
|
|
return "", err
|
|
}
|
|
|
|
resolveErr := err
|
|
if parentName != "" {
|
|
if _, parentErr := runComposeConfig(composePath, parentName, envFile); parentErr == nil {
|
|
return parentName, nil
|
|
} else {
|
|
resolveErr = parentErr
|
|
}
|
|
}
|
|
|
|
if fallbackName != "" && fallbackName != parentName {
|
|
if _, fallbackErr := runComposeConfig(composePath, fallbackName, envFile); fallbackErr == nil {
|
|
return fallbackName, nil
|
|
} else {
|
|
return "", fallbackErr
|
|
}
|
|
}
|
|
if parentName == "" && fallbackName == "" {
|
|
return "", buserr.New("ErrComposeProjectNameEmpty")
|
|
}
|
|
return "", resolveErr
|
|
}
|
|
|
|
func runComposeConfig(composePath, projectName, envFile string) ([]byte, error) {
|
|
configCmd := getComposeCmdWithEnv(composePath, "config", envFile, projectName)
|
|
stdout, err := configCmd.Output()
|
|
if err != nil {
|
|
var stderr []byte
|
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
|
stderr = exitErr.Stderr
|
|
}
|
|
return nil, fmt.Errorf("docker-compose config failed, std: %s, err: %v", mergeComposeOutput(stdout, stderr), err)
|
|
}
|
|
return stdout, nil
|
|
}
|
|
|
|
func mergeComposeOutput(stdout, stderr []byte) string {
|
|
outputs := make([]string, 0, 2)
|
|
if output := strings.TrimSpace(string(stdout)); output != "" {
|
|
outputs = append(outputs, output)
|
|
}
|
|
if output := strings.TrimSpace(string(stderr)); output != "" {
|
|
outputs = append(outputs, output)
|
|
}
|
|
return strings.Join(outputs, "\n")
|
|
}
|
|
|
|
func loadComposeProjectName(config []byte) (string, error) {
|
|
var project struct {
|
|
Name string `yaml:"name"`
|
|
}
|
|
if err := yaml.Unmarshal(config, &project); err != nil {
|
|
return "", buserr.WithDetail("ErrComposeProjectNameParse", err.Error(), err)
|
|
}
|
|
return strings.TrimSpace(project.Name), nil
|
|
}
|
|
|
|
func primaryComposePath(composePath string) string {
|
|
if index := strings.Index(composePath, ","); index >= 0 {
|
|
return composePath[:index]
|
|
}
|
|
return composePath
|
|
}
|
|
|
|
func normalizeComposeProjectName(name string) string {
|
|
name = re.GetRegex(re.ComposeDisallowedCharsPattern).
|
|
ReplaceAllString(strings.ToLower(strings.TrimSpace(name)), "")
|
|
return strings.TrimLeft(name, "_-")
|
|
}
|
|
|
|
func isComposeProjectNameEmptyError(err error) bool {
|
|
message := strings.ToLower(err.Error())
|
|
return strings.Contains(message, "project name must not be empty") ||
|
|
strings.Contains(message, "project name can't be empty")
|
|
}
|
|
|
|
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")
|
|
}
|
|
if req.Operation == "delete" {
|
|
if err := removeContainerForCompose(req.Name, req.Path); err != nil && !req.Force {
|
|
return err
|
|
}
|
|
if req.WithFile {
|
|
for _, item := range strings.Split(req.Path, ",") {
|
|
if len(item) != 0 {
|
|
_ = os.RemoveAll(path.Dir(item))
|
|
}
|
|
}
|
|
}
|
|
_ = composeRepo.DeleteRecord(repo.WithByName(req.Name))
|
|
return nil
|
|
}
|
|
if req.Operation == "up" {
|
|
if stdout, err := compose.Up(req.Path, req.Name); err != nil {
|
|
return fmt.Errorf("docker-compose up failed, std: %s, err: %v", stdout, err)
|
|
}
|
|
} else if req.Operation == "rebuild" {
|
|
if stdout, err := compose.DownAndUp(req.Path, req.Name); err != nil {
|
|
return fmt.Errorf("docker-compose rebuild failed, std: %s, err: %v", stdout, err)
|
|
}
|
|
} else {
|
|
if stdout, err := compose.Operate(req.Path, req.Operation, req.Name); err != nil {
|
|
return fmt.Errorf("docker-compose %s failed, std: %s, err: %v", req.Operation, stdout, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error {
|
|
if cmd.CheckIllegal(req.Name, req.Path) {
|
|
return buserr.New("ErrCmdIllegal")
|
|
}
|
|
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskUpdate, task.TaskScopeCompose, req.TaskID, 1)
|
|
if err != nil {
|
|
global.LOG.Errorf("new task for update compose failed, err: %v", err)
|
|
return err
|
|
}
|
|
go func() {
|
|
taskItem.AddSubTask(i18n.GetMsgByKey("TaskUpdate"), func(t *task.Task) error {
|
|
oldFile, err := os.ReadFile(req.DetailPath)
|
|
if err != nil {
|
|
return fmt.Errorf("load file with path %s failed, %v", req.DetailPath, err)
|
|
}
|
|
file, err := os.OpenFile(req.DetailPath, os.O_WRONLY|os.O_TRUNC, 0640)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
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.DetailPath)
|
|
if err := newComposeEnv(req.DetailPath, 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 {
|
|
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)
|
|
}
|
|
|
|
return nil
|
|
}, nil)
|
|
_ = taskItem.Execute()
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (u *ContainerService) ComposePin(req dto.ComposePin) error {
|
|
record, _ := composeRepo.GetRecord(repo.WithByName(req.Name))
|
|
if record.ID == 0 {
|
|
if !req.IsPinned {
|
|
return nil
|
|
}
|
|
return composeRepo.CreateRecord(&model.Compose{Name: req.Name, IsPinned: true})
|
|
}
|
|
if !req.IsPinned && len(record.Path) == 0 {
|
|
return composeRepo.DeleteRecord(repo.WithByName(req.Name))
|
|
}
|
|
return composeRepo.UpdateRecord(req.Name, map[string]interface{}{"is_pinned": req.IsPinned})
|
|
}
|
|
|
|
func (u *ContainerService) ComposeLogClean(req dto.ComposeLogClean) error {
|
|
client, err := docker.NewDockerClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer client.Close()
|
|
|
|
options := container.ListOptions{All: true}
|
|
options.Filters = filters.NewArgs()
|
|
options.Filters.Add("label", composeProjectLabel)
|
|
|
|
list, err := client.ContainerList(context.Background(), options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ctx := context.Background()
|
|
for _, item := range list {
|
|
if name, ok := item.Labels[composeProjectLabel]; ok {
|
|
if name != req.Name {
|
|
continue
|
|
}
|
|
containerItem, err := client.ContainerInspect(ctx, item.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := client.ContainerStop(ctx, containerItem.ID, container.StopOptions{}); err != nil {
|
|
return err
|
|
}
|
|
file, err := os.OpenFile(containerItem.LogPath, os.O_RDWR|os.O_CREATE, constant.FilePerm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
if err = file.Truncate(0); err != nil {
|
|
return err
|
|
}
|
|
_, _ = file.Seek(0, 0)
|
|
|
|
files, _ := filepath.Glob(fmt.Sprintf("%s.*", containerItem.LogPath))
|
|
for _, file := range files {
|
|
_ = os.Remove(file)
|
|
}
|
|
}
|
|
}
|
|
return u.ComposeOperation(dto.ComposeOperation{
|
|
Name: req.Name,
|
|
Path: req.Path,
|
|
Operation: "restart",
|
|
})
|
|
}
|
|
|
|
func (u *ContainerService) LoadComposeEnv(name string) (string, error) {
|
|
envFilePath := path.Join(path.Dir(name), ".env")
|
|
file, err := os.ReadFile(envFilePath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(file), nil
|
|
}
|
|
|
|
func (u *ContainerService) loadPath(req *dto.ComposeCreate) error {
|
|
if req.From == "template" || req.From == "edit" {
|
|
composePath := composeCreatePath(*req)
|
|
dir := filepath.Dir(composePath)
|
|
if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
|
|
if err = os.MkdirAll(dir, os.ModePerm); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
file, err := os.OpenFile(composePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, constant.FilePerm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
write := bufio.NewWriter(file)
|
|
_, _ = write.WriteString(string(req.File))
|
|
write.Flush()
|
|
req.Path = composePath
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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)
|
|
}
|
|
return nil
|
|
}
|
|
var options container.ListOptions
|
|
options.All = true
|
|
options.Filters = filters.NewArgs()
|
|
options.Filters.Add("label", "com.docker.compose.project="+composeName)
|
|
client, err := docker.NewDockerClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer client.Close()
|
|
ctx := context.Background()
|
|
containers, err := client.ContainerList(ctx, options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, c := range containers {
|
|
_ = client.ContainerRemove(ctx, c.ID, container.RemoveOptions{RemoveVolumes: true, Force: true})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func recreateCompose(content, path, projectName 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, projectName); err != nil {
|
|
return errors.New(string(stdout))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func loadEnv(list []dto.ComposeInfo) []dto.ComposeInfo {
|
|
for i := 0; i < len(list); i++ {
|
|
tmpPath := list[i].Path
|
|
if strings.Contains(list[i].Path, ",") {
|
|
tmpPath = strings.Split(list[i].Path, ",")[0]
|
|
}
|
|
envFilePath := path.Join(path.Dir(tmpPath), ".env")
|
|
file, err := os.ReadFile(envFilePath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
list[i].Env = string(file)
|
|
}
|
|
return list
|
|
}
|
|
|
|
func newComposeEnv(pathItem string, env string) error {
|
|
envFilePath := path.Join(path.Dir(pathItem), ".env")
|
|
file, err := os.OpenFile(envFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, constant.FilePerm)
|
|
if err != nil {
|
|
global.LOG.Errorf("failed to create env file: %v", err)
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
if _, err := file.WriteString(env); err != nil {
|
|
global.LOG.Errorf("failed to write env to file: %v", err)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|