feat: 增加已安装应用同步逻辑

This commit is contained in:
zhengkunwang223 2022-09-29 18:16:56 +08:00 committed by zhengkunwang223
parent 5855e9b0d8
commit 69b34e07c9
30 changed files with 460 additions and 99 deletions

BIN
apps/icons/halo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 66 KiB

BIN
apps/icons/wordpress.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -1,5 +1,5 @@
{
"version": "0.1",
"version": "ddd",
"tags": [
{
"key": "WebSite",
@ -24,6 +24,8 @@
"icon": "mysql.png",
"author": "Oracle",
"type": "internal",
"required": [""],
"crossVersionUpdate": false,
"source": "https://www.mysql.com"
},
{
@ -35,6 +37,8 @@
"icon": "nginx.png",
"author": "Nginx",
"type": "internal",
"required": [""],
"crossVersionUpdate": true,
"source": "http://nginx.org/"
}
]

View File

@ -0,0 +1,5 @@
# What is nginx?
- - -
Nginx (pronounced "engine-x") is an open source reverse proxy server for HTTP, HTTPS, SMTP, POP3, and IMAP protocols, as well as a load balancer, HTTP cache, and a web server (origin server). The nginx project started with a strong focus on high concurrency, high performance and low memory usage. It is licensed under the 2-clause BSD-like license and it runs on Linux, BSD variants, Mac OS X, Solaris, AIX, HP-UX, as well as on other *nix flavors. It also has a proof of concept port for Microsoft Windows.
[wikipedia.org/wiki/Nginx](http://wikipedia.org/wiki/Nginx)

View File

@ -0,0 +1,22 @@
version: '3'
services:
1panel_wordpress:
image: wordpress:6.0.1
container_name: 1panel_wordpress
ports:
- "8080:80"
restart: always
networks:
- 1panel
volumes:
- ./data:/var/www/html
environment:
WORDPRESS_DB_HOST: 1panel_mysql
WORDPRESS_DB_NAME: wpdb
WORDPRESS_DB_USER: root
WORDPRESS_DB_PASSWORD: Password@123
WORDPRESS_DEBUG: 1
networks:
1panel:
external: true

View File

@ -25,7 +25,7 @@ func (b *BaseApi) AppSearch(c *gin.Context) {
}
func (b *BaseApi) AppSync(c *gin.Context) {
if err := appService.Sync(); err != nil {
if err := appService.SyncAppList(); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
@ -106,3 +106,11 @@ func (b *BaseApi) InstallOperate(c *gin.Context) {
helper.SuccessWithData(c, nil)
}
func (b *BaseApi) InstalledSync(c *gin.Context) {
if err := appService.SyncAllInstalled(); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, "")
}

View File

@ -88,9 +88,18 @@ var (
Down AppOperate = "down"
Restart AppOperate = "restart"
Delete AppOperate = "delete"
Sync AppOperate = "sync"
)
type AppInstallOperate struct {
InstallId uint `json:"installId" validate:"required"`
Operate AppOperate `json:"operate" validate:"required"`
}
//type AppContainer struct {
// Names []string `json:"names"`
// Image string `json:"image"`
// Ports string `json:"ports"`
// Status string `json:"status"`
// State string `json:"state"`
//}

View File

@ -0,0 +1,9 @@
package model
type AppContainer struct {
BaseModel
ServiceName string `json:"serviceName"`
ContainerName string `json:"containerName"`
AppInstallId uint `json:"appInstallId"`
Image string `json:"image"`
}

View File

@ -1,15 +1,28 @@
package model
import (
"github.com/1Panel-dev/1Panel/global"
"path"
)
type AppInstall struct {
BaseModel
Name string `json:"name" gorm:"type:varchar(64);not null"`
ContainerName string `json:"containerName" gorm:"type:varchar(256);not null"`
Version string `json:"version" gorm:"type:varchar(256);not null"`
AppId uint `json:"appId" gorm:"type:integer;not null"`
AppDetailId uint `json:"appDetailId" gorm:"type:integer;not null"`
Params string `json:"params" gorm:"type:longtext;not null"`
Status string `json:"status" gorm:"type:varchar(256);not null"`
Description string `json:"description" gorm:"type:varchar(256);not null"`
Message string `json:"message" gorm:"type:longtext;not null"`
App App `json:"-"`
Name string `json:"name" gorm:"type:varchar(64);not null"`
Version string `json:"version" gorm:"type:varchar(256);not null"`
AppId uint `json:"appId" gorm:"type:integer;not null"`
AppDetailId uint `json:"appDetailId" gorm:"type:integer;not null"`
Params string `json:"params" gorm:"type:longtext;not null"`
Status string `json:"status" gorm:"type:varchar(256);not null"`
Description string `json:"description" gorm:"type:varchar(256);not null"`
Message string `json:"message" gorm:"type:longtext;not null"`
App App `json:"-"`
Containers []AppContainer `json:"containers"`
}
func (i AppInstall) GetPath() string {
return path.Join(global.CONF.System.AppDir, i.App.Key, i.Name)
}
func (i AppInstall) GetComposePath() string {
return path.Join(global.CONF.System.AppDir, i.App.Key, i.Name, "docker-compose.yml")
}

View File

@ -0,0 +1,21 @@
package repo
import (
"context"
"github.com/1Panel-dev/1Panel/app/model"
"github.com/1Panel-dev/1Panel/global"
"gorm.io/gorm"
)
type AppContainerRepo struct {
}
func (a AppContainerRepo) Create(container *model.AppContainer) error {
db := global.DB.Model(&model.AppContainer{})
return db.Create(&container).Error
}
func (a AppContainerRepo) BatchCreate(ctx context.Context, containers []*model.AppContainer) error {
db := ctx.Value("db").(*gorm.DB)
return db.Model(&model.AppContainer{}).Create(&containers).Error
}

View File

@ -13,7 +13,17 @@ func (a AppInstallRepo) GetBy(opts ...DBOption) ([]model.AppInstall, error) {
db = opt(db)
}
var install []model.AppInstall
err := db.Preload("App").Find(&install).Error
err := db.Preload("App").Preload("Containers").Find(&install).Error
return install, err
}
func (a AppInstallRepo) GetFirst(opts ...DBOption) (model.AppInstall, error) {
db := global.DB.Model(&model.AppInstall{})
for _, opt := range opts {
db = opt(db)
}
var install model.AppInstall
err := db.Preload("App").Preload("Containers").First(&install).Error
return install, err
}
@ -43,6 +53,6 @@ func (a AppInstallRepo) Page(page, size int, opts ...DBOption) (int64, []model.A
}
count := int64(0)
db = db.Count(&count)
err := db.Debug().Limit(size).Offset(size * (page - 1)).Preload("App").Find(&apps).Error
err := db.Debug().Limit(size).Offset(size * (page - 1)).Preload("App").Preload("Containers").Find(&apps).Error
return count, apps, err
}

View File

@ -14,6 +14,7 @@ type RepoGroup struct {
TagRepo
AppDetailRepo
AppInstallRepo
AppContainerRepo
}
var RepoGroupApp = new(RepoGroup)

View File

@ -11,14 +11,17 @@ import (
"github.com/1Panel-dev/1Panel/global"
"github.com/1Panel-dev/1Panel/utils/common"
"github.com/1Panel-dev/1Panel/utils/compose"
"github.com/1Panel-dev/1Panel/utils/docker"
"github.com/1Panel-dev/1Panel/utils/files"
"github.com/joho/godotenv"
"golang.org/x/net/context"
"gopkg.in/yaml.v3"
"os"
"path"
"reflect"
"sort"
"strconv"
"strings"
)
type AppService struct {
@ -158,7 +161,8 @@ func (a AppService) Operate(req dto.AppInstallOperate) error {
}
install := appInstall[0]
dockerComposePath := path.Join(global.CONF.System.AppDir, install.App.Key, install.ContainerName, "docker-compose.yml")
dockerComposePath := install.GetComposePath()
switch req.Operate {
case dto.Up:
out, err := compose.Up(dockerComposePath)
@ -180,7 +184,7 @@ func (a AppService) Operate(req dto.AppInstallOperate) error {
install.Status = constant.Running
case dto.Delete:
op := files.NewFileOp()
appDir := path.Join(global.CONF.System.AppDir, install.App.Key, install.ContainerName)
appDir := install.GetPath()
dir, _ := os.Stat(appDir)
if dir == nil {
return appInstallRepo.Delete(commonRepo.WithByID(install.ID))
@ -196,6 +200,11 @@ func (a AppService) Operate(req dto.AppInstallOperate) error {
_ = op.DeleteDir(appDir)
_ = appInstallRepo.Delete(commonRepo.WithByID(install.ID))
return nil
case dto.Sync:
if err := a.SyncInstalled(install.ID); err != nil {
return err
}
return nil
default:
return errors.New("operate not support")
}
@ -233,30 +242,28 @@ func (a AppService) Install(name string, appDetailId uint, params map[string]int
if err != nil {
return err
}
containerName := constant.ContainerPrefix + app.Key + "-" + common.RandStr(6)
appInstall := model.AppInstall{
Name: name,
AppId: appDetail.AppId,
AppDetailId: appDetail.ID,
Version: appDetail.Version,
Status: constant.Installing,
Params: string(paramByte),
ContainerName: containerName,
Message: "",
Name: name,
AppId: appDetail.AppId,
AppDetailId: appDetail.ID,
Version: appDetail.Version,
Status: constant.Installing,
Params: string(paramByte),
}
resourceDir := path.Join(global.CONF.System.ResourceDir, "apps", app.Key, appDetail.Version)
installDir := path.Join(global.CONF.System.AppDir, app.Key)
installAppDir := path.Join(installDir, appDetail.Version)
op := files.NewFileOp()
if err := op.Copy(resourceDir, installAppDir); err != nil {
installVersionDir := path.Join(installDir, appDetail.Version)
fileOp := files.NewFileOp()
if err := fileOp.Copy(resourceDir, installVersionDir); err != nil {
return err
}
containerNameDir := path.Join(installDir, containerName)
if err := op.Rename(installAppDir, containerNameDir); err != nil {
appDir := path.Join(installDir, name)
if err := fileOp.Rename(installVersionDir, appDir); err != nil {
return err
}
composeFilePath := path.Join(containerNameDir, "docker-compose.yml")
envPath := path.Join(containerNameDir, ".env")
composeFilePath := path.Join(appDir, "docker-compose.yml")
envPath := path.Join(appDir, ".env")
envParams := make(map[string]string, len(params))
for k, v := range params {
@ -269,13 +276,58 @@ func (a AppService) Install(name string, appDetailId uint, params map[string]int
envParams[k] = t.(string)
}
}
envParams["CONTAINER_NAME"] = containerName
if err := godotenv.Write(envParams, envPath); err != nil {
return err
}
fileContent, err := os.ReadFile(composeFilePath)
if err != nil {
return err
}
composeMap := make(map[string]interface{})
if err := yaml.Unmarshal(fileContent, &composeMap); err != nil {
return err
}
servicesMap := composeMap["services"].(map[string]interface{})
changeKeys := make(map[string]string, len(servicesMap))
var appContainers []*model.AppContainer
for k, v := range servicesMap {
serviceName := k + "-" + common.RandStr(4)
changeKeys[k] = serviceName
value := v.(map[string]interface{})
containerName := constant.ContainerPrefix + k + "-" + common.RandStr(4)
value["container_name"] = containerName
var image string
if i, ok := value["image"]; ok {
image = i.(string)
}
appContainers = append(appContainers, &model.AppContainer{
ServiceName: serviceName,
ContainerName: containerName,
Image: image,
})
}
for k, v := range changeKeys {
servicesMap[v] = servicesMap[k]
delete(servicesMap, k)
}
serviceByte, err := yaml.Marshal(servicesMap)
if err != nil {
return err
}
if err := fileOp.WriteFile(composeFilePath, strings.NewReader(string(serviceByte)), 0775); err != nil {
return err
}
if err := appInstallRepo.Create(&appInstall); err != nil {
return err
}
for _, c := range appContainers {
c.AppInstallId = appInstall.ID
}
if err := appContainerRepo.BatchCreate(context.WithValue(context.Background(), "db", global.DB), appContainers); err != nil {
return err
}
go upApp(composeFilePath, appInstall)
return nil
}
@ -296,12 +348,96 @@ func upApp(composeFilePath string, appInstall model.AppInstall) {
}
}
func (a AppService) SyncInstalled() error {
func (a AppService) SyncAllInstalled() error {
allList, err := appInstallRepo.GetBy()
if err != nil {
return err
}
go func() {
for _, i := range allList {
if err := a.SyncInstalled(i.ID); err != nil {
global.LOG.Errorf("sync install app[%s] error,mgs: %s", i.Name, err.Error())
}
}
}()
return nil
}
func (a AppService) Sync() error {
func (a AppService) SyncInstalled(installId uint) error {
appInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(installId))
if err != nil {
return err
}
var containerNames []string
for _, a := range appInstall.Containers {
containerNames = append(containerNames, a.ContainerName)
}
cli, err := docker.NewClient()
if err != nil {
return err
}
containers, err := cli.ListContainersByName(containerNames)
if err != nil {
return err
}
var errorContainers []string
var notFoundContainers []string
for _, n := range containers {
if n.State != "running" {
errorContainers = append(errorContainers, n.Names...)
}
}
for _, old := range containerNames {
exist := false
for _, new := range containers {
if common.ExistWithStrArray(old, new.Names) {
exist = true
break
}
}
if !exist {
notFoundContainers = append(notFoundContainers, old)
}
}
if len(containers) == 0 {
appInstall.Status = constant.Error
appInstall.Message = "container is not found"
return appInstallRepo.Save(appInstall)
}
if len(errorContainers) == 0 && len(notFoundContainers) == 0 {
appInstall.Status = constant.Running
return appInstallRepo.Save(appInstall)
}
if len(errorContainers) == len(containerNames) {
appInstall.Status = constant.Error
}
if len(notFoundContainers) == len(containerNames) {
appInstall.Status = constant.Stopped
}
var errMsg strings.Builder
if len(errorContainers) > 0 {
errMsg.Write([]byte(string(rune(len(errorContainers))) + " error containers:"))
for _, e := range errorContainers {
errMsg.Write([]byte(e))
}
errMsg.Write([]byte("\n"))
}
if len(notFoundContainers) > 0 {
errMsg.Write([]byte(string(rune(len(notFoundContainers))) + " not found containers:"))
for _, e := range notFoundContainers {
errMsg.Write([]byte(e))
}
errMsg.Write([]byte("\n"))
}
appInstall.Message = errMsg.String()
return appInstallRepo.Save(appInstall)
}
func (a AppService) SyncAppList() error {
//TODO 从 oss 拉取最新列表
var appConfig model.AppConfig
appConfig.OssPath = global.CONF.System.AppOss

View File

@ -30,5 +30,6 @@ var (
appTagRepo = repo.RepoGroupApp.AppTagRepo
appDetailRepo = repo.RepoGroupApp.AppDetailRepo
tagRepo = repo.RepoGroupApp.TagRepo
appInstallRepo = repo.AppInstallRepo{}
appInstallRepo = repo.RepoGroupApp.AppInstallRepo
appContainerRepo = repo.RepoGroupApp.AppContainerRepo
)

View File

@ -2,7 +2,7 @@ package constant
const (
Running = "Running"
Warning = "Warning"
UnHealthy = "UnHealthy"
Error = "Error"
Stopped = "Stopped"
Installing = "Installing"

View File

@ -50,12 +50,17 @@ require (
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Microsoft/go-winio v0.6.0 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.1.1 // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/docker v20.10.18+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dsnet/compress v0.0.1 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/fatih/color v1.13.0 // indirect
@ -100,6 +105,8 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.2 // indirect
github.com/pierrec/lz4/v4 v4.1.15 // indirect
@ -118,9 +125,10 @@ require (
go.opentelemetry.io/otel v1.0.0 // indirect
go.opentelemetry.io/otel/trace v1.0.0 // indirect
golang.org/x/image v0.0.0-20190802002840-cff245a6509b // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect
golang.org/x/tools v0.1.10 // indirect
golang.org/x/tools v0.1.12 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/ini.v1 v1.66.6 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect

View File

@ -41,6 +41,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
@ -85,6 +87,14 @@ github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/Lu
github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68=
github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v20.10.18+incompatible h1:SN84VYXTBNGn92T/QwIRPlum9zfemfitN7pbsp26WSc=
github.com/docker/docker v20.10.18+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
@ -358,6 +368,10 @@ github.com/nwaples/rardecode/v2 v2.0.0-beta.2 h1:e3mzJFJs4k83GXBEiTaQ5HgSc/kOK8q
github.com/nwaples/rardecode/v2 v2.0.0-beta.2/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
@ -528,6 +542,8 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -711,6 +727,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -150,6 +150,6 @@ var AddTableCronjob = &gormigrate.Migration{
var AddTableApp = &gormigrate.Migration{
ID: "20200921-add-table-app",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(&model.App{}, &model.AppDetail{}, &model.Tag{}, &model.AppTag{}, &model.AppConfig{}, &model.AppInstall{})
return tx.AutoMigrate(&model.App{}, &model.AppDetail{}, &model.Tag{}, &model.AppTag{}, &model.AppConfig{}, &model.AppInstall{}, &model.AppContainer{}, &model.AppContainer{})
},
}

View File

@ -22,5 +22,6 @@ func (a *AppRouter) InitAppRouter(Router *gin.RouterGroup) {
appRouter.POST("/install", baseApi.InstallApp)
appRouter.POST("/installed", baseApi.PageInstalled)
appRouter.POST("/installed/op", baseApi.InstallOperate)
appRouter.POST("/installed/sync", baseApi.InstalledSync)
}
}

View File

@ -71,3 +71,12 @@ func ScanPort(port string) bool {
defer ln.Close()
return false
}
func ExistWithStrArray(str string, arr []string) bool {
for _, a := range arr {
if strings.Contains(a, str) {
return true
}
}
return false
}

View File

@ -0,0 +1,49 @@
package docker
import (
"context"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
)
type Client struct {
cli *client.Client
}
func NewClient() (Client, error) {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return Client{}, err
}
return Client{
cli: cli,
}, nil
}
func (c Client) ListAllContainers() ([]types.Container, error) {
var options types.ContainerListOptions
containers, err := c.cli.ContainerList(context.Background(), options)
if err != nil {
return nil, err
}
return containers, nil
}
func (c Client) ListContainersByName(names []string) ([]types.Container, error) {
var options types.ContainerListOptions
options.All = true
if len(names) > 0 {
var array []filters.KeyValuePair
for _, n := range names {
array = append(array, filters.Arg("name", n))
}
options.Filters = filters.NewArgs(array...)
}
containers, err := c.cli.ContainerList(context.Background(), options)
if err != nil {
return nil, err
}
return containers, nil
}

View File

@ -29,6 +29,10 @@ func NewFileOp() FileOp {
}
}
func (f FileOp) OpenFile(dst string) (fs.File, error) {
return f.Fs.Open(dst)
}
func (f FileOp) CreateDir(dst string, mode fs.FileMode) error {
return f.Fs.MkdirAll(dst, mode)
}

View File

@ -63,7 +63,6 @@ export namespace App {
export interface AppInstalled extends CommonModel {
name: string;
containerName: string;
version: string;
appId: string;
appDetailId: string;
@ -72,9 +71,8 @@ export namespace App {
description: string;
message: string;
appName: string;
total: number;
ready: number;
icon: string;
constainers: any[];
}
export interface AppInstalledOp {

View File

@ -29,3 +29,7 @@ export const GetAppInstalled = (info: ReqPage) => {
export const InstalledOp = (op: App.AppInstalledOp) => {
return http.post<any>('apps/installed/op', op);
};
export const SyncInstalledApp = () => {
return http.post<any>('apps/installed/sync', {});
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

View File

@ -408,5 +408,6 @@ export default {
description: '描述',
delete: '删除',
deleteWarn: '删除操作会把数据一并删除,此操作不可回滚,是否继续?',
syncSuccess: '同步成功',
},
};

View File

@ -1,53 +1,60 @@
<template>
<el-row :gutter="20">
<el-col :span="12">
<el-input v-model="req.name" @blur="searchByName"></el-input>
</el-col>
<el-col :span="12">
<el-select v-model="req.tags" multiple style="width: 100%" @change="changeTag">
<el-option v-for="item in tags" :key="item.key" :label="item.name" :value="item.key"></el-option>
</el-select>
</el-col>
<el-col v-for="(app, index) in apps" :key="index" :xs="8" :sm="8" :lg="4">
<div @click="getAppDetail(app.id)">
<el-card :body-style="{ padding: '0px' }" class="a-card">
<el-row :gutter="24">
<el-col :span="8">
<div class="icon">
<el-image class="image" :src="'data:image/png;base64,' + app.icon"></el-image>
</div>
</el-col>
<el-col :span="16">
<div class="a-detail">
<div class="d-name">
<font size="3" style="font-weight: 700">{{ app.name }}</font>
<div v-loading="loading">
<el-row :gutter="20">
<el-col :span="12">
<el-input v-model="req.name" @blur="searchByName"></el-input>
</el-col>
<el-col :span="11">
<el-select v-model="req.tags" multiple style="width: 100%" @change="changeTag">
<el-option v-for="item in tags" :key="item.key" :label="item.name" :value="item.key"></el-option>
</el-select>
</el-col>
<el-col :span="1">
<el-button @click="sync">{{ $t('app.sync') }}</el-button>
</el-col>
<el-col v-for="(app, index) in apps" :key="index" :xs="8" :sm="8" :lg="4">
<div @click="getAppDetail(app.id)">
<el-card :body-style="{ padding: '0px' }" class="a-card">
<el-row :gutter="24">
<el-col :span="8">
<div class="icon">
<el-image class="image" :src="'data:image/png;base64,' + app.icon"></el-image>
</div>
<div class="d-description">
<font size="1">
<span>
{{ app.shortDesc }}
</span>
</font>
</el-col>
<el-col :span="16">
<div class="a-detail">
<div class="d-name">
<font size="3" style="font-weight: 700">{{ app.name }}</font>
</div>
<div class="d-description">
<font size="1">
<span>
{{ app.shortDesc }}
</span>
</font>
</div>
<div class="d-tag">
<el-tag v-for="(tag, ind) in app.tags" :key="ind" round :colr="getColor(ind)">
{{ tag.name }}
</el-tag>
</div>
</div>
<div class="d-tag">
<el-tag v-for="(tag, ind) in app.tags" :key="ind" round :colr="getColor(ind)">
{{ tag.name }}
</el-tag>
</div>
</div>
</el-col>
</el-row>
</el-card>
</div>
</el-col>
</el-row>
</el-col>
</el-row>
</el-card>
</div>
</el-col>
</el-row>
</div>
</template>
<script lang="ts" setup>
import { App } from '@/api/interface/app';
import { onMounted, reactive, ref } from 'vue';
import router from '@/routers';
import { SearchApp } from '@/api/modules/app';
import { SearchApp, SyncApp } from '@/api/modules/app';
import { ElMessage } from 'element-plus';
import i18n from '@/lang';
let req = reactive({
name: '',
@ -59,6 +66,7 @@ let req = reactive({
let apps = ref<App.App[]>([]);
let tags = ref<App.Tag[]>([]);
const colorArr = ['#6495ED', '#54FF9F', '#BEBEBE', '#FFF68F', '#FFFF00', '#8B0000'];
let loading = ref(false);
const getColor = (index: number) => {
return colorArr[index];
@ -78,6 +86,17 @@ const getAppDetail = (id: number) => {
router.push({ name: 'AppDetail', params });
};
const sync = () => {
loading.value = true;
SyncApp()
.then(() => {
ElMessage.success(i18n.global.t('app.syncSuccess'));
})
.finally(() => {
loading.value = false;
});
};
const changeTag = () => {
search(req);
};

View File

@ -21,25 +21,17 @@
</template>
<script lang="ts" setup>
import { SyncApp } from '@/api/modules/app';
import LayoutContent from '@/layout/layout-content.vue';
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const activeName = ref('all');
const sync = () => {
SyncApp().then((res) => {
console.log(res);
});
};
const routerTo = (path: string) => {
router.push({ path: path });
};
onMounted(() => {
sync();
const path = router.currentRoute.value.path;
if (path === '/apps/all') {
activeName.value = 'all';

View File

@ -1,14 +1,12 @@
<template>
<div style="float: right; margin-bottom: 5px">
<el-button @click="sync">{{ $t('app.sync') }}</el-button>
</div>
<ComplexTable :pagination-config="paginationConfig" :data="data" @search="search" v-loading="loading">
<el-table-column :label="$t('app.name')" prop="name"></el-table-column>
<!-- <el-table-column :label="$t('app.description')" prop="description"></el-table-column> -->
<el-table-column :label="$t('app.appName')" prop="appName"></el-table-column>
<el-table-column :label="$t('app.version')" prop="version"></el-table-column>
<el-table-column :label="$t('app.container')">
<template #default="{ row }">
{{ row.ready / row.total }}
</template>
</el-table-column>
<el-table-column :label="$t('app.status')">
<template #default="{ row }">
<el-popover
@ -32,7 +30,7 @@
show-overflow-tooltip
/>
<fu-table-operations
width="200px"
width="250px"
:ellipsis="10"
:buttons="buttons"
:label="$t('commons.table.operate')"
@ -52,7 +50,7 @@
</template>
<script lang="ts" setup>
import { GetAppInstalled, InstalledOp } from '@/api/modules/app';
import { GetAppInstalled, InstalledOp, SyncInstalledApp } from '@/api/modules/app';
import { onMounted, reactive, ref } from 'vue';
import ComplexTable from '@/components/complex-table/index.vue';
import { dateFromat } from '@/utils/util';
@ -72,6 +70,18 @@ let operateReq = reactive({
operate: '',
});
const sync = () => {
loading.value = true;
SyncInstalledApp()
.then(() => {
ElMessage.success(i18n.global.t('app.syncSuccess'));
search();
})
.finally(() => {
loading.value = false;
});
};
const search = () => {
const req = {
page: paginationConfig.currentPage,
@ -122,12 +132,21 @@ const getMsg = (op: string) => {
case 'delete':
tip = i18n.global.t('app.deleteWarn');
break;
case 'sync':
tip = i18n.global.t('app.sync');
break;
default:
}
return tip;
};
const buttons = [
{
label: i18n.global.t('app.sync'),
click: (row: any) => {
openOperate(row, 'sync');
},
},
{
label: i18n.global.t('app.restart'),
click: (row: any) => {