mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-11-07 00:05:14 +00:00
327 lines
13 KiB
Go
327 lines
13 KiB
Go
package service
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
|
"github.com/1Panel-dev/1Panel/agent/global"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
const composeUpgradeCurrent = `services:
|
|
demo:
|
|
image: demo:1.0
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
cpus: ${CPUS}
|
|
memory: ${MEMORY_LIMIT}
|
|
`
|
|
|
|
func composeUpgradeDB(t *testing.T, resourceURL string) (*gorm.DB, model.App, model.AppInstall, []model.AppDetail) {
|
|
t.Helper()
|
|
db := iconSyncDB(t)
|
|
if err := db.AutoMigrate(&model.AppIgnoreUpgrade{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
app := model.App{Key: "demo", Name: "Demo", Type: "runtime", Resource: constant.AppResourceRemote, CrossVersionUpdate: true}
|
|
if err := db.Create(&app).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var details []model.AppDetail
|
|
for _, version := range []string{"1.1", "1.0", "1.3", "1.2"} {
|
|
detail := model.AppDetail{
|
|
AppId: app.ID, Version: version, LastModified: 42, Params: `{"formFields":[]}`, Status: constant.AppNormal,
|
|
DownloadUrl: resourceURL + "/demo/" + version + "/demo-" + version + ".tar.gz",
|
|
}
|
|
if err := db.Create(&detail).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
details = append(details, detail)
|
|
}
|
|
if err := db.Create(&model.AppIgnoreUpgrade{AppID: app.ID, AppDetailID: details[2].ID, Scope: "version"}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
install := model.AppInstall{
|
|
AppId: app.ID, AppDetailId: details[1].ID, Name: "demo-install", Version: "1.0", App: app,
|
|
ServiceName: "demo", ContainerName: "demo-container", Status: constant.StatusRunning,
|
|
Env: `{"CONTAINER_NAME":"demo-container"}`, DockerCompose: composeUpgradeCurrent,
|
|
}
|
|
if err := db.Omit(clause.Associations).Create(&install).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return db, app, install, details
|
|
}
|
|
|
|
func TestUpgradeComposeLoadsOnlyEffectiveTarget(t *testing.T) {
|
|
var mu sync.Mutex
|
|
requests := make(map[string]int)
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
mu.Lock()
|
|
requests[r.URL.Path]++
|
|
mu.Unlock()
|
|
if r.URL.Query().Get("v") != "42" {
|
|
t.Errorf("missing synchronized version: %s", r.URL)
|
|
}
|
|
version := strings.Split(r.URL.Path, "/")[2]
|
|
_, _ = fmt.Fprintf(w, "services:\n source:\n image: demo:%s\n", version)
|
|
}))
|
|
t.Cleanup(server.Close)
|
|
db, _, install, details := composeUpgradeDB(t, server.URL)
|
|
// An unselected bad cached template must not prevent listing or default preview.
|
|
if err := db.Model(&details[0]).UpdateColumn("docker_compose", "invalid: [").Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
service := NewIAppInstalledService()
|
|
noCompose := false
|
|
versions, err := service.GetUpdateVersions(request.AppUpdateVersion{AppInstallID: install.ID, LoadCompose: &noCompose})
|
|
if err != nil || len(versions) != 2 || versions[0].Version != "1.2" || versions[1].Version != "1.1" {
|
|
t.Fatalf("pure versions did not preserve filtering/sorting: %+v, %v", versions, err)
|
|
}
|
|
for _, version := range versions {
|
|
if version.DockerCompose != "" {
|
|
t.Fatal("pure version request returned a rendered template")
|
|
}
|
|
}
|
|
if len(requests) != 0 {
|
|
t.Fatalf("pure version request fetched templates: %v", requests)
|
|
}
|
|
for i := 0; i < 2; i++ {
|
|
versions, err = service.GetUpdateVersions(request.AppUpdateVersion{AppInstallID: install.ID})
|
|
if err != nil || len(versions) != 2 || !strings.Contains(versions[0].DockerCompose, "demo:1.2") || versions[1].DockerCompose != "" {
|
|
t.Fatalf("default target preview = %+v, %v", versions, err)
|
|
}
|
|
}
|
|
if requests["/demo/1.2/docker-compose.yml"] != 1 || len(requests) != 1 {
|
|
t.Fatalf("default preview did not reuse only its own cache: %v", requests)
|
|
}
|
|
if err := db.Model(&details[0]).UpdateColumn("docker_compose", "").Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
versions, err = service.GetUpdateVersions(request.AppUpdateVersion{AppInstallID: install.ID, UpdateVersion: "1.1"})
|
|
if err != nil || !strings.Contains(versions[1].DockerCompose, "demo:1.1") || versions[0].DockerCompose != "" {
|
|
t.Fatalf("selected target preview = %+v, %v", versions, err)
|
|
}
|
|
if requests["/demo/1.1/docker-compose.yml"] != 1 || len(requests) != 2 {
|
|
t.Fatalf("selection prefetched other candidates: %v", requests)
|
|
}
|
|
if _, err := service.GetUpdateVersions(request.AppUpdateVersion{AppInstallID: install.ID, UpdateVersion: "1.3"}); err == nil {
|
|
t.Fatal("ignored target was accepted")
|
|
}
|
|
if len(requests) != 2 {
|
|
t.Fatal("invalid target triggered a template request")
|
|
}
|
|
}
|
|
|
|
func TestUpgradeComposeFetchFailurePreservesInstanceAndCache(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusServiceUnavailable) }))
|
|
t.Cleanup(server.Close)
|
|
db, _, install, details := composeUpgradeDB(t, server.URL)
|
|
if _, err := NewIAppInstalledService().GetUpdateVersions(request.AppUpdateVersion{AppInstallID: install.ID}); err == nil {
|
|
t.Fatal("failed target template load was accepted")
|
|
}
|
|
var stored model.AppInstall
|
|
if err := db.First(&stored, install.ID).Error; err != nil || stored.DockerCompose != install.DockerCompose || stored.Status != install.Status || stored.Version != install.Version {
|
|
t.Fatalf("preview modified installed instance: %+v, %v", stored, err)
|
|
}
|
|
var detail model.AppDetail
|
|
if err := db.First(&detail, details[3].ID).Error; err != nil || detail.DockerCompose != "" {
|
|
t.Fatalf("failed response cached a template: %+v, %v", detail, err)
|
|
}
|
|
}
|
|
|
|
func TestUpgradePrepareComposeFailureDoesNotCutover(t *testing.T) {
|
|
for _, failure := range []string{"package download", "invalid package template"} {
|
|
t.Run(failure, func(t *testing.T) {
|
|
archive := appCacheTestArchive(t, "demo/1.2/docker-compose.yml", "services: []")
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if failure == "package download" {
|
|
w.WriteHeader(http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
_, _ = w.Write(archive)
|
|
}))
|
|
t.Cleanup(server.Close)
|
|
db, _, install, details := composeUpgradeDB(t, server.URL)
|
|
upgrade := &appUpgradeContext{original: install, detail: details[3]}
|
|
if err := upgrade.prepare(&task.Task{Logger: global.LOG}); err == nil {
|
|
t.Fatal("invalid upgrade resources passed prepare")
|
|
}
|
|
if upgrade.stopAttempted || upgrade.downAttempted || upgrade.stageDir != "" {
|
|
t.Fatal("failed template preparation reached live cutover")
|
|
}
|
|
var stored model.AppInstall
|
|
if err := db.First(&stored, install.ID).Error; err != nil || stored.DockerCompose != install.DockerCompose || stored.Env != install.Env || stored.Version != install.Version || stored.Status != install.Status {
|
|
t.Fatalf("failed prepare modified the instance: %+v, %v", stored, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExplicitConfigComposeDetectsCustomAndReusesCurrentCache(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
custom bool
|
|
failure bool
|
|
}{
|
|
{name: "original"},
|
|
{name: "custom", custom: true},
|
|
{name: "fetch failure", failure: true},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
calls := 0
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
calls++
|
|
if r.URL.Path != "/demo/1.0/docker-compose.yml" {
|
|
t.Errorf("configuration loaded a different version: %s", r.URL)
|
|
}
|
|
if tc.failure {
|
|
w.WriteHeader(http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
_, _ = io.WriteString(w, "services:\n source:\n image: demo:1.0\n")
|
|
}))
|
|
t.Cleanup(server.Close)
|
|
db, _, install, _ := composeUpgradeDB(t, server.URL)
|
|
if tc.custom {
|
|
install.DockerCompose = strings.Replace(install.DockerCompose, "image: demo:1.0", "image: demo:1.0\n environment:\n USER_SETTING: preserve", 1)
|
|
if err := db.Omit(clause.Associations).Save(&install).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if isEditCompose(install) || calls != 0 {
|
|
t.Fatal("list-time custom check fetched a missing template")
|
|
}
|
|
result, err := NewIAppInstalledService().GetParams(install.ID)
|
|
if tc.failure {
|
|
if err == nil || result != nil {
|
|
t.Fatal("failed current-template load was treated as an unedited configuration")
|
|
}
|
|
return
|
|
}
|
|
if err != nil || result.RawCompose == "" || result.IsEdit != tc.custom || result.DockerCompose != install.DockerCompose {
|
|
t.Fatalf("explicit custom state = %+v, %v; want custom=%v", result, err, tc.custom)
|
|
}
|
|
if _, err := NewIAppInstalledService().GetParams(install.ID); err != nil || calls != 1 {
|
|
t.Fatalf("configuration cache was not reused: calls=%d, %v", calls, err)
|
|
}
|
|
if isEditCompose(install) != tc.custom || calls != 1 {
|
|
t.Fatal("cached list-time comparison changed custom state or made network requests")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRemovedCurrentComposeDoesNotBlockUpgrade(t *testing.T) {
|
|
for _, custom := range []bool{false, true} {
|
|
t.Run(fmt.Sprintf("custom=%v", custom), func(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/demo/1.0/docker-compose.yml" {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
version := strings.Split(r.URL.Path, "/")[2]
|
|
_, _ = fmt.Fprintf(w, "services:\n source:\n image: demo:%s\n", version)
|
|
}))
|
|
t.Cleanup(server.Close)
|
|
db, _, install, details := composeUpgradeDB(t, server.URL)
|
|
if err := db.Model(&details[1]).UpdateColumn("status", constant.AppTakeDown).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if custom {
|
|
install.DockerCompose += " environment:\n USER_SETTING: preserve\n"
|
|
if err := db.Omit(clause.Associations).Save(&install).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
service := NewIAppInstalledService()
|
|
config, err := service.GetParams(install.ID)
|
|
if err != nil || config.RawCompose != install.DockerCompose || config.DockerCompose != install.DockerCompose || !config.IsEdit {
|
|
t.Fatalf("removed template must preserve the instance and warn about custom configuration: %+v, %v", config, err)
|
|
}
|
|
var current model.AppDetail
|
|
if err := db.First(¤t, details[1].ID).Error; err != nil || current.DockerCompose != "" {
|
|
t.Fatalf("instance Compose was cached as the official template: %+v, %v", current, err)
|
|
}
|
|
noCompose := false
|
|
versions, err := service.GetUpdateVersions(request.AppUpdateVersion{AppInstallID: install.ID, LoadCompose: &noCompose})
|
|
if err != nil || len(versions) != 2 {
|
|
t.Fatalf("removed current version blocked available versions: %+v, %v", versions, err)
|
|
}
|
|
for _, version := range versions {
|
|
previews, err := service.GetUpdateVersions(request.AppUpdateVersion{AppInstallID: install.ID, UpdateVersion: version.Version})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, preview := range previews {
|
|
if preview.Version == version.Version && !strings.Contains(preview.DockerCompose, "demo:"+version.Version) {
|
|
t.Fatalf("upgrade did not use the selected new template: %+v", preview)
|
|
}
|
|
}
|
|
}
|
|
var stored model.AppInstall
|
|
if err := db.First(&stored, install.ID).Error; err != nil || stored.DockerCompose != install.DockerCompose || stored.Version != install.Version || stored.Status != install.Status {
|
|
t.Fatalf("upgrade preview modified the running instance: %+v, %v", stored, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRemovedCurrentComposeRejectsInvalidInstanceCompose(t *testing.T) {
|
|
for _, compose := range []string{"", "services: []", "services: ["} {
|
|
t.Run(compose, func(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}))
|
|
t.Cleanup(server.Close)
|
|
db, _, install, _ := composeUpgradeDB(t, server.URL)
|
|
if err := db.Model(&install).UpdateColumn("docker_compose", compose).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if config, err := NewIAppInstalledService().GetParams(install.ID); err == nil || config != nil {
|
|
t.Fatalf("invalid instance Compose was accepted: %+v, %v", config, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVllmManagedComposeDoesNotRequireOfficialTemplate(t *testing.T) {
|
|
var calls atomic.Int32
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
calls.Add(1)
|
|
w.WriteHeader(http.StatusServiceUnavailable)
|
|
}))
|
|
t.Cleanup(server.Close)
|
|
db, app, install, _ := composeUpgradeDB(t, server.URL)
|
|
if err := db.Model(&app).UpdateColumn("key", vllmAppKeyForUpgrade).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
install.DockerCompose = strings.Replace(install.DockerCompose, "image: demo:1.0", "image: demo:1.0\n command: [--managed-user-option]", 1)
|
|
if err := db.Omit(clause.Associations).Save(&install).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
service := NewIAppInstalledService()
|
|
config, err := service.GetParams(install.ID)
|
|
if err != nil || config.RawCompose != install.DockerCompose || config.DockerCompose != install.DockerCompose || config.IsEdit {
|
|
t.Fatalf("managed configuration lost the instance template: %+v, %v", config, err)
|
|
}
|
|
versions, err := service.GetUpdateVersions(request.AppUpdateVersion{AppInstallID: install.ID})
|
|
if err != nil || len(versions) != 2 || versions[0].DockerCompose != install.DockerCompose {
|
|
t.Fatalf("managed upgrade preview lost the instance template: %+v, %v", versions, err)
|
|
}
|
|
if calls.Load() != 0 {
|
|
t.Fatalf("managed template attempted %d official resource requests", calls.Load())
|
|
}
|
|
}
|