mirror of https://github.com/1Panel-dev/1Panel
appstorecrontabdatabasedockerdocker-composedocker-containerdocker-imagedocker-uifilemanagerlamplnmppanel
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
436 lines
12 KiB
436 lines
12 KiB
package service |
|
|
|
import ( |
|
"bytes" |
|
"fmt" |
|
"io" |
|
"net/http" |
|
"os" |
|
"os/exec" |
|
"path" |
|
"path/filepath" |
|
"strings" |
|
"time" |
|
|
|
"github.com/1Panel-dev/1Panel/backend/app/dto/request" |
|
"github.com/1Panel-dev/1Panel/backend/app/model" |
|
"github.com/1Panel-dev/1Panel/backend/buserr" |
|
"github.com/1Panel-dev/1Panel/backend/constant" |
|
"github.com/1Panel-dev/1Panel/backend/global" |
|
"github.com/1Panel-dev/1Panel/backend/utils/docker" |
|
"github.com/1Panel-dev/1Panel/backend/utils/files" |
|
httpUtil "github.com/1Panel-dev/1Panel/backend/utils/http" |
|
"github.com/pkg/errors" |
|
"github.com/subosito/gotenv" |
|
"gopkg.in/yaml.v3" |
|
) |
|
|
|
func handleNodeAndJava(create request.RuntimeCreate, runtime *model.Runtime, fileOp files.FileOp, appVersionDir string) (err error) { |
|
runtimeDir := path.Join(constant.RuntimeDir, create.Type) |
|
if err = fileOp.CopyDir(appVersionDir, runtimeDir); err != nil { |
|
return |
|
} |
|
versionDir := path.Join(runtimeDir, filepath.Base(appVersionDir)) |
|
projectDir := path.Join(runtimeDir, create.Name) |
|
defer func() { |
|
if err != nil { |
|
_ = fileOp.DeleteDir(projectDir) |
|
} |
|
}() |
|
if err = fileOp.Rename(versionDir, projectDir); err != nil { |
|
return |
|
} |
|
composeContent, envContent, _, err := handleParams(create, projectDir) |
|
if err != nil { |
|
return |
|
} |
|
runtime.DockerCompose = string(composeContent) |
|
runtime.Env = string(envContent) |
|
runtime.Status = constant.RuntimeCreating |
|
runtime.CodeDir = create.CodeDir |
|
|
|
nodeDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(runtime.AppDetailID)) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
go func() { |
|
if _, _, err := httpUtil.HandleGet(nodeDetail.DownloadCallBackUrl, http.MethodGet, constant.TimeOut5s); err != nil { |
|
global.LOG.Errorf("http request failed(handleNode), err: %v", err) |
|
return |
|
} |
|
}() |
|
go startRuntime(runtime) |
|
|
|
return |
|
} |
|
|
|
func handlePHP(create request.RuntimeCreate, runtime *model.Runtime, fileOp files.FileOp, appVersionDir string) (err error) { |
|
buildDir := path.Join(appVersionDir, "build") |
|
if !fileOp.Stat(buildDir) { |
|
return buserr.New(constant.ErrDirNotFound) |
|
} |
|
runtimeDir := path.Join(constant.RuntimeDir, create.Type) |
|
tempDir := filepath.Join(runtimeDir, fmt.Sprintf("%d", time.Now().UnixNano())) |
|
if err = fileOp.CopyDir(buildDir, tempDir); err != nil { |
|
return |
|
} |
|
oldDir := path.Join(tempDir, "build") |
|
projectDir := path.Join(runtimeDir, create.Name) |
|
defer func() { |
|
if err != nil { |
|
_ = fileOp.DeleteDir(projectDir) |
|
} |
|
}() |
|
if oldDir != projectDir { |
|
if err = fileOp.Rename(oldDir, projectDir); err != nil { |
|
return |
|
} |
|
if err = fileOp.DeleteDir(tempDir); err != nil { |
|
return |
|
} |
|
} |
|
composeContent, envContent, forms, err := handleParams(create, projectDir) |
|
if err != nil { |
|
return |
|
} |
|
runtime.DockerCompose = string(composeContent) |
|
runtime.Env = string(envContent) |
|
runtime.Params = string(forms) |
|
runtime.Status = constant.RuntimeBuildIng |
|
|
|
go buildRuntime(runtime, "", false) |
|
return |
|
} |
|
|
|
func startRuntime(runtime *model.Runtime) { |
|
if err := runComposeCmdWithLog("up", runtime.GetComposePath(), runtime.GetLogPath()); err != nil { |
|
runtime.Status = constant.RuntimeError |
|
runtime.Message = err.Error() |
|
_ = runtimeRepo.Save(runtime) |
|
return |
|
} |
|
|
|
if err := SyncRuntimeContainerStatus(runtime); err != nil { |
|
runtime.Status = constant.RuntimeError |
|
runtime.Message = err.Error() |
|
_ = runtimeRepo.Save(runtime) |
|
return |
|
} |
|
} |
|
|
|
func reCreateRuntime(runtime *model.Runtime) { |
|
var err error |
|
defer func() { |
|
if err != nil { |
|
runtime.Status = constant.RuntimeError |
|
runtime.Message = err.Error() |
|
_ = runtimeRepo.Save(runtime) |
|
} |
|
}() |
|
if err = runComposeCmdWithLog("down", runtime.GetComposePath(), runtime.GetLogPath()); err != nil { |
|
return |
|
} |
|
if err = runComposeCmdWithLog("up", runtime.GetComposePath(), runtime.GetLogPath()); err != nil { |
|
return |
|
} |
|
if err := SyncRuntimeContainerStatus(runtime); err != nil { |
|
return |
|
} |
|
} |
|
|
|
func runComposeCmdWithLog(operate string, composePath string, logPath string) error { |
|
cmd := exec.Command("docker-compose", "-f", composePath, operate) |
|
if operate == "up" { |
|
cmd = exec.Command("docker-compose", "-f", composePath, operate, "-d") |
|
} |
|
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666) |
|
if err != nil { |
|
global.LOG.Errorf("Failed to open log file: %v", err) |
|
return err |
|
} |
|
defer logFile.Close() |
|
multiWriterStdout := io.MultiWriter(os.Stdout, logFile) |
|
cmd.Stdout = multiWriterStdout |
|
var stderrBuf bytes.Buffer |
|
multiWriterStderr := io.MultiWriter(&stderrBuf, logFile, os.Stderr) |
|
cmd.Stderr = multiWriterStderr |
|
|
|
err = cmd.Run() |
|
if err != nil { |
|
return errors.New(buserr.New(constant.ErrRuntimeStart).Error() + ":" + stderrBuf.String()) |
|
} |
|
return nil |
|
} |
|
|
|
func SyncRuntimeContainerStatus(runtime *model.Runtime) error { |
|
env, err := gotenv.Unmarshal(runtime.Env) |
|
if err != nil { |
|
return err |
|
} |
|
var containerNames []string |
|
if containerName, ok := env["CONTAINER_NAME"]; !ok { |
|
return buserr.New("ErrContainerNameNotFound") |
|
} else { |
|
containerNames = append(containerNames, containerName) |
|
} |
|
cli, err := docker.NewClient() |
|
if err != nil { |
|
return err |
|
} |
|
defer cli.Close() |
|
containers, err := cli.ListContainersByName(containerNames) |
|
if err != nil { |
|
return err |
|
} |
|
if len(containers) == 0 { |
|
return buserr.WithNameAndErr("ErrContainerNotFound", containerNames[0], nil) |
|
} |
|
container := containers[0] |
|
|
|
switch container.State { |
|
case "exited": |
|
runtime.Status = constant.RuntimeError |
|
case "running": |
|
runtime.Status = constant.RuntimeRunning |
|
case "paused": |
|
runtime.Status = constant.RuntimeStopped |
|
default: |
|
if runtime.Status != constant.RuntimeBuildIng { |
|
runtime.Status = constant.RuntimeStopped |
|
} |
|
} |
|
|
|
return runtimeRepo.Save(runtime) |
|
} |
|
|
|
func buildRuntime(runtime *model.Runtime, oldImageID string, rebuild bool) { |
|
runtimePath := runtime.GetPath() |
|
composePath := runtime.GetComposePath() |
|
logPath := path.Join(runtimePath, "build.log") |
|
|
|
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666) |
|
if err != nil { |
|
global.LOG.Errorf("failed to open log file: %v", err) |
|
return |
|
} |
|
defer func() { |
|
_ = logFile.Close() |
|
}() |
|
|
|
cmd := exec.Command("docker-compose", "-f", composePath, "build") |
|
multiWriterStdout := io.MultiWriter(os.Stdout, logFile) |
|
cmd.Stdout = multiWriterStdout |
|
var stderrBuf bytes.Buffer |
|
multiWriterStderr := io.MultiWriter(&stderrBuf, logFile, os.Stderr) |
|
cmd.Stderr = multiWriterStderr |
|
|
|
err = cmd.Run() |
|
if err != nil { |
|
runtime.Status = constant.RuntimeError |
|
runtime.Message = buserr.New(constant.ErrImageBuildErr).Error() + ":" + stderrBuf.String() |
|
} else { |
|
runtime.Status = constant.RuntimeNormal |
|
runtime.Message = "" |
|
if oldImageID != "" { |
|
client, err := docker.NewClient() |
|
if err == nil { |
|
defer client.Close() |
|
newImageID, err := client.GetImageIDByName(runtime.Image) |
|
if err == nil && newImageID != oldImageID { |
|
global.LOG.Infof("delete imageID [%s] ", oldImageID) |
|
if err := client.DeleteImage(oldImageID); err != nil { |
|
global.LOG.Errorf("delete imageID [%s] error %v", oldImageID, err) |
|
} else { |
|
global.LOG.Infof("delete old image success") |
|
} |
|
} |
|
} else { |
|
global.LOG.Errorf("delete imageID [%s] error %v", oldImageID, err) |
|
} |
|
} |
|
if rebuild && runtime.ID > 0 { |
|
websites, _ := websiteRepo.GetBy(websiteRepo.WithRuntimeID(runtime.ID)) |
|
if len(websites) > 0 { |
|
installService := NewIAppInstalledService() |
|
installMap := make(map[uint]string) |
|
for _, website := range websites { |
|
if website.AppInstallID > 0 { |
|
installMap[website.AppInstallID] = website.PrimaryDomain |
|
} |
|
} |
|
for installID, domain := range installMap { |
|
go func(installID uint, domain string) { |
|
global.LOG.Infof("rebuild php runtime [%s] domain [%s]", runtime.Name, domain) |
|
if err := installService.Operate(request.AppInstalledOperate{ |
|
InstallId: installID, |
|
Operate: constant.Rebuild, |
|
}); err != nil { |
|
global.LOG.Errorf("rebuild php runtime [%s] domain [%s] error %v", runtime.Name, domain, err) |
|
} |
|
}(installID, domain) |
|
} |
|
} |
|
} |
|
} |
|
_ = runtimeRepo.Save(runtime) |
|
} |
|
|
|
func handleParams(create request.RuntimeCreate, projectDir string) (composeContent []byte, envContent []byte, forms []byte, err error) { |
|
fileOp := files.NewFileOp() |
|
composeContent, err = fileOp.GetContent(path.Join(projectDir, "docker-compose.yml")) |
|
if err != nil { |
|
return |
|
} |
|
envPath := path.Join(projectDir, ".env") |
|
if !fileOp.Stat(envPath) { |
|
_ = fileOp.CreateFile(envPath) |
|
} |
|
env, err := gotenv.Read(envPath) |
|
if err != nil { |
|
return |
|
} |
|
switch create.Type { |
|
case constant.RuntimePHP: |
|
create.Params["IMAGE_NAME"] = create.Image |
|
forms, err = fileOp.GetContent(path.Join(projectDir, "config.json")) |
|
if err != nil { |
|
return |
|
} |
|
if extends, ok := create.Params["PHP_EXTENSIONS"]; ok { |
|
if extendsArray, ok := extends.([]interface{}); ok { |
|
strArray := make([]string, len(extendsArray)) |
|
for i, v := range extendsArray { |
|
strArray[i] = strings.ToLower(fmt.Sprintf("%v", v)) |
|
} |
|
create.Params["PHP_EXTENSIONS"] = strings.Join(strArray, ",") |
|
} |
|
} |
|
create.Params["CONTAINER_PACKAGE_URL"] = create.Source |
|
case constant.RuntimeNode: |
|
create.Params["CODE_DIR"] = create.CodeDir |
|
create.Params["NODE_VERSION"] = create.Version |
|
create.Params["PANEL_APP_PORT_HTTP"] = create.Port |
|
if create.NodeConfig.Install { |
|
create.Params["RUN_INSTALL"] = "1" |
|
} else { |
|
create.Params["RUN_INSTALL"] = "0" |
|
} |
|
create.Params["CONTAINER_PACKAGE_URL"] = create.Source |
|
|
|
composeContent, err = handleCompose(env, composeContent, create, projectDir) |
|
if err != nil { |
|
return |
|
} |
|
case constant.RuntimeJava: |
|
create.Params["CODE_DIR"] = create.CodeDir |
|
create.Params["JAVA_VERSION"] = create.Version |
|
create.Params["PANEL_APP_PORT_HTTP"] = create.Port |
|
composeContent, err = handleCompose(env, composeContent, create, projectDir) |
|
if err != nil { |
|
return |
|
} |
|
case constant.RuntimeGo: |
|
create.Params["CODE_DIR"] = create.CodeDir |
|
create.Params["GO_VERSION"] = create.Version |
|
create.Params["PANEL_APP_PORT_HTTP"] = create.Port |
|
composeContent, err = handleCompose(env, composeContent, create, projectDir) |
|
if err != nil { |
|
return |
|
} |
|
} |
|
|
|
newMap := make(map[string]string) |
|
handleMap(create.Params, newMap) |
|
for k, v := range newMap { |
|
env[k] = v |
|
} |
|
|
|
envStr, err := gotenv.Marshal(env) |
|
if err != nil { |
|
return |
|
} |
|
if err = gotenv.Write(env, envPath); err != nil { |
|
return |
|
} |
|
envContent = []byte(envStr) |
|
return |
|
} |
|
|
|
func handleCompose(env gotenv.Env, composeContent []byte, create request.RuntimeCreate, projectDir string) (composeByte []byte, err error) { |
|
existMap := make(map[string]interface{}) |
|
composeMap := make(map[string]interface{}) |
|
if err = yaml.Unmarshal(composeContent, &composeMap); err != nil { |
|
return |
|
} |
|
services, serviceValid := composeMap["services"].(map[string]interface{}) |
|
if !serviceValid { |
|
err = buserr.New(constant.ErrFileParse) |
|
return |
|
} |
|
serviceName := "" |
|
serviceValue := make(map[string]interface{}) |
|
for name, service := range services { |
|
serviceName = name |
|
serviceValue = service.(map[string]interface{}) |
|
_, ok := serviceValue["ports"].([]interface{}) |
|
if ok { |
|
var ports []interface{} |
|
|
|
switch create.Type { |
|
case constant.RuntimeNode: |
|
ports = append(ports, "${HOST_IP}:${PANEL_APP_PORT_HTTP}:${NODE_APP_PORT}") |
|
case constant.RuntimeJava: |
|
ports = append(ports, "${HOST_IP}:${PANEL_APP_PORT_HTTP}:${JAVA_APP_PORT}") |
|
case constant.RuntimeGo: |
|
ports = append(ports, "${HOST_IP}:${PANEL_APP_PORT_HTTP}:${GO_APP_PORT}") |
|
|
|
} |
|
|
|
for i, port := range create.ExposedPorts { |
|
containerPortStr := fmt.Sprintf("CONTAINER_PORT_%d", i) |
|
hostPortStr := fmt.Sprintf("HOST_PORT_%d", i) |
|
existMap[containerPortStr] = struct{}{} |
|
existMap[hostPortStr] = struct{}{} |
|
ports = append(ports, fmt.Sprintf("${HOST_IP}:${%s}:${%s}", hostPortStr, containerPortStr)) |
|
create.Params[containerPortStr] = port.ContainerPort |
|
create.Params[hostPortStr] = port.HostPort |
|
} |
|
serviceValue["ports"] = ports |
|
} |
|
break |
|
} |
|
for k := range env { |
|
if strings.Contains(k, "CONTAINER_PORT_") || strings.Contains(k, "HOST_PORT_") { |
|
if _, ok := existMap[k]; !ok { |
|
delete(env, k) |
|
} |
|
} |
|
} |
|
|
|
services[serviceName] = serviceValue |
|
composeMap["services"] = services |
|
composeByte, err = yaml.Marshal(composeMap) |
|
if err != nil { |
|
return |
|
} |
|
fileOp := files.NewFileOp() |
|
_ = fileOp.SaveFile(path.Join(projectDir, "docker-compose.yml"), string(composeByte), 0644) |
|
return |
|
} |
|
|
|
func checkContainerName(name string) error { |
|
dockerCli, err := docker.NewClient() |
|
if err != nil { |
|
return err |
|
} |
|
defer dockerCli.Close() |
|
names, err := dockerCli.ListContainersByName([]string{name}) |
|
if err != nil { |
|
return err |
|
} |
|
if len(names) > 0 { |
|
return buserr.New(constant.ErrContainerName) |
|
} |
|
return nil |
|
}
|
|
|