From aeb479e00a870b2eaad56cb1aa211d5d4d2c501d Mon Sep 17 00:00:00 2001 From: CorrectRoad Date: Thu, 11 Apr 2024 16:03:17 +0800 Subject: [PATCH] add upgradable app api (#182) --- api/app_management/openapi.yaml | 4 + cmd/message-bus-docgen/main.go | 4 +- cmd/migration-tool/log.go | 7 +- route/v2/appstore.go | 44 ++++++++-- route/v2/compose_app.go | 9 +- service/appstore_management.go | 122 +++++++++++++++++++++++++++- service/appstore_management_test.go | 52 ++++++++++++ service/appstore_test.go | 12 +-- service/compose_app.go | 112 ------------------------- service/compose_app_test.go | 48 ----------- 10 files changed, 228 insertions(+), 186 deletions(-) diff --git a/api/app_management/openapi.yaml b/api/app_management/openapi.yaml index 95b586af..fbfb0850 100644 --- a/api/app_management/openapi.yaml +++ b/api/app_management/openapi.yaml @@ -1480,6 +1480,7 @@ components: - version - store_app_id - status + - icon properties: title: type: string @@ -1490,6 +1491,9 @@ components: example: "v10.2" store_app_id: $ref: "#/components/schemas/StoreAppID" + icon: + type: string + example: "https://cdn.jsdelivr.net/gh/IceWhaleTech/CasaOS-AppStore@main/Apps/Syncthing/icon.png" status: type: string enum: diff --git a/cmd/message-bus-docgen/main.go b/cmd/message-bus-docgen/main.go index 7c6902ad..112ff5fe 100644 --- a/cmd/message-bus-docgen/main.go +++ b/cmd/message-bus-docgen/main.go @@ -8,12 +8,12 @@ import ( ) func main() { - eventTypes := lo.Map(common.EventTypes, func(item message_bus.EventType, index int) external.EventType { + eventTypes := lo.Map(common.EventTypes, func(item message_bus.EventType, _ int) external.EventType { return external.EventType{ Name: item.Name, SourceID: item.SourceID, PropertyTypeList: lo.Map( - item.PropertyTypeList, func(item message_bus.PropertyType, index int) external.PropertyType { + item.PropertyTypeList, func(item message_bus.PropertyType, _ int) external.PropertyType { return external.PropertyType{ Name: item.Name, Description: item.Description, diff --git a/cmd/migration-tool/log.go b/cmd/migration-tool/log.go index ed699830..2a10889b 100644 --- a/cmd/migration-tool/log.go +++ b/cmd/migration-tool/log.go @@ -6,11 +6,10 @@ import ( ) type Logger struct { + _debug *log.Logger + _info *log.Logger + _error *log.Logger DebugMode bool - - _debug *log.Logger - _info *log.Logger - _error *log.Logger } func NewLogger() *Logger { diff --git a/route/v2/appstore.go b/route/v2/appstore.go index 5953a86b..9ff5c83a 100644 --- a/route/v2/appstore.go +++ b/route/v2/appstore.go @@ -2,6 +2,7 @@ package v2 import ( "context" + "encoding/json" "fmt" "net/http" "path/filepath" @@ -202,7 +203,6 @@ func (a *AppManagement) ComposeAppMainStableTag(ctx echo.Context, id codegen.Sto return ctx.JSON(http.StatusInternalServerError, codegen.ResponseInternalServerError{ Message: utils.Ptr(err.Error()), }) - } _, tag := docker.ExtractImageAndTag(mainService.Image) @@ -232,7 +232,6 @@ func (a *AppManagement) ComposeAppServiceStableTag(ctx echo.Context, id codegen. return ctx.JSON(http.StatusInternalServerError, codegen.ResponseInternalServerError{ Message: utils.Ptr("service not found"), }) - } _, tag := docker.ExtractImageAndTag(service.Image) @@ -377,16 +376,45 @@ func (a *AppManagement) UpgradableAppList(ctx echo.Context) error { continue } - if composeApp.IsUpdateAvailable() { + storeInfo, err := composeApp.StoreInfo(true) + if err != nil { + logger.Error("failed to get store info", zap.Error(err), zap.String("appStoreID", id)) + continue + } + + title, err := json.Marshal(storeInfo.Title) + if err != nil { + title = []byte("unknown") + } + + storeComposeApp, err := service.MyService.V2AppStore().ComposeApp(id) + if err != nil || storeComposeApp == nil { + logger.Error("failed to get compose app", zap.Error(err), zap.String("appStoreID", id)) + continue + } + tag, err := storeComposeApp.MainTag() + if err != nil { + // TODO + logger.Error("failed to get compose app main tag", zap.Error(err), zap.String("appStoreID", id)) + continue + } + + status := codegen.Idle + if service.MyService.AppStoreManagement().IsUpdating(composeApp.Name) { + status = codegen.Updating + } + + if service.MyService.AppStoreManagement().IsUpdateAvailable(composeApp) { upgradableAppList = append(upgradableAppList, codegen.UpgradableAppInfo{ - Title: composeApp.Name, - Version: "", + Title: string(title), + Version: tag, StoreAppID: lo.ToPtr(id), - Status: "upgradable", + Status: status, + Icon: storeInfo.Icon, }) } } - return ctx.JSON(http.StatusNotImplemented, codegen.ResponseOK{ - Message: lo.ToPtr("not implemented"), + return ctx.JSON(http.StatusOK, codegen.UpgradableAppListOK{ + Data: &upgradableAppList, }) } diff --git a/route/v2/compose_app.go b/route/v2/compose_app.go index 3eeb44fb..92c71640 100644 --- a/route/v2/compose_app.go +++ b/route/v2/compose_app.go @@ -84,7 +84,7 @@ func (a *AppManagement) MyComposeApp(ctx echo.Context, id codegen.ComposeAppID) } // check if updateAvailable - updateAvailable := composeApp.IsUpdateAvailable() + updateAvailable := service.MyService.AppStoreManagement().IsUpdateAvailable(composeApp) message := fmt.Sprintf("!! JSON format is for debugging purpose only - use `Accept: %s` HTTP header to get YAML instead !!", common.MIMEApplicationYAML) return ctx.JSON(http.StatusOK, codegen.ComposeAppOK{ @@ -453,7 +453,7 @@ func (a *AppManagement) UpdateComposeApp(ctx echo.Context, id codegen.ComposeApp if params.Force != nil && !*params.Force { // check if updateAvailable - if !composeApp.IsUpdateAvailable() { + if !service.MyService.AppStoreManagement().IsUpdateAvailable(composeApp) { message := fmt.Sprintf("compose app `%s` is up to date", id) return ctx.JSON(http.StatusOK, codegen.ComposeAppUpdateOK{Message: &message}) } @@ -461,6 +461,9 @@ func (a *AppManagement) UpdateComposeApp(ctx echo.Context, id codegen.ComposeApp backgroundCtx := common.WithProperties(context.Background(), PropertiesFromQueryParams(ctx)) + service.MyService.AppStoreManagement().StartUpgrade(id) + defer service.MyService.AppStoreManagement().FinishUpgrade(id) + if err := composeApp.Update(backgroundCtx); err != nil { logger.Error("failed to update compose app", zap.Error(err), zap.String("appID", id)) message := err.Error() @@ -695,7 +698,7 @@ func composeAppsWithStoreInfo(ctx context.Context) (map[string]codegen.ComposeAp composeAppWithStoreInfo.StoreInfo = storeInfo // check if updateAvailable - updateAvailable := composeApp.IsUpdateAvailable() + updateAvailable := service.MyService.AppStoreManagement().IsUpdateAvailable(composeApp) composeAppWithStoreInfo.UpdateAvailable = &updateAvailable diff --git a/service/appstore_management.go b/service/appstore_management.go index fb5ef2b5..57b9c52d 100644 --- a/service/appstore_management.go +++ b/service/appstore_management.go @@ -4,13 +4,18 @@ import ( "context" "fmt" "strings" + "sync" + "time" "github.com/IceWhaleTech/CasaOS-AppManagement/codegen" "github.com/IceWhaleTech/CasaOS-AppManagement/common" "github.com/IceWhaleTech/CasaOS-AppManagement/pkg/config" + "github.com/IceWhaleTech/CasaOS-AppManagement/pkg/docker" "github.com/IceWhaleTech/CasaOS-Common/utils" "github.com/IceWhaleTech/CasaOS-Common/utils/file" "github.com/IceWhaleTech/CasaOS-Common/utils/logger" + "github.com/bluele/gcache" + "github.com/docker/docker/client" "github.com/samber/lo" "go.uber.org/zap" ) @@ -19,6 +24,8 @@ type AppStoreManagement struct { onAppStoreRegister []func(string) error onAppStoreUnregister []func(string) error + isAppUpgradable gcache.Cache + isAppUpgrading sync.Map defaultAppStore AppStore } @@ -366,6 +373,9 @@ func (a *AppStoreManagement) UpdateCatalog() error { } } + // clean cache + a.isAppUpgradable.Purge() + return nil } @@ -376,9 +386,9 @@ func (a *AppStoreManagement) ComposeApp(id string) (*ComposeApp, error) { } for _, appStore := range appStoreMap { - composeApp, err := appStore.ComposeApp(id) - if err != nil { - logger.Error("error while getting appstore compose app", zap.Error(err)) + composeApp, appErr := appStore.ComposeApp(id) + if appErr != nil { + logger.Error("error while getting appstore compose app", zap.Error(appErr)) continue } @@ -408,6 +418,110 @@ func (a *AppStoreManagement) WorkDir() (string, error) { panic("not implemented and will never be implemented - this is a virtual appstore") } +func (a *AppStoreManagement) IsUpdateAvailable(composeApp *ComposeApp) bool { + storeID := composeApp.Name + if value, err := a.isAppUpgradable.Get(storeID); err == nil { + switch value := value.(type) { + case bool: + return value + default: + logger.Error("invalid type in cache", zap.String("storeID", storeID), zap.Any("value", value)) + return false + } + } + + isUpdate, err := a.isUpdateAvailable(composeApp) + if err != nil { + logger.Error("failed to check if update is available", zap.Error(err)) + return false + } + _ = a.isAppUpgradable.Set(storeID, isUpdate) + return isUpdate +} + +func (a *AppStoreManagement) isUpdateAvailable(composeApp *ComposeApp) (bool, error) { + // handle no tag logic and for easy to test + storeInfo, err := composeApp.StoreInfo(false) + if err != nil { + logger.Error("failed to get store info of compose app, thus no update available", zap.Error(err)) + return false, nil + } + + // if app is uncontrolled, no update available + if storeInfo.IsUncontrolled != nil && *storeInfo.IsUncontrolled { + return false, nil + } + + if storeInfo == nil || storeInfo.StoreAppID == nil || *storeInfo.StoreAppID == "" { + return false, err + } + + storeComposeApp, err := a.ComposeApp(*storeInfo.StoreAppID) + if err != nil { + logger.Error("failed to get store compose app, thus no update available", zap.Error(err)) + return false, err + } + + if storeComposeApp == nil { + logger.Error("store compose app not found, thus no update available", zap.String("storeAppID", *storeInfo.StoreAppID)) + return false, nil + } + + return a.IsUpdateAvailableWith(composeApp, storeComposeApp) +} + +func (a *AppStoreManagement) IsUpdateAvailableWith(composeApp *ComposeApp, storeComposeApp *ComposeApp) (bool, error) { + currentTag, err := composeApp.MainTag() + if err != nil { + logger.Error("failed to get current tag", zap.Error(err)) + return false, err + } + mainService, err := composeApp.MainService() + if err != nil { + logger.Error("failed to get main service", zap.Error(err)) + return false, err + } + if currentTag == "latest" { + ctx := context.Background() + cli, clientErr := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if clientErr != nil { + logger.Error("failed to create docker client", zap.Error(clientErr)) + return false, clientErr + } + defer cli.Close() + + image, _ := docker.ExtractImageAndTag(mainService.Image) + + imageInfo, _, clientErr := cli.ImageInspectWithRaw(ctx, image) + if clientErr != nil { + logger.Error("failed to inspect image", zap.Error(clientErr)) + return false, clientErr + } + match, clientErr := docker.CompareDigest(mainService.Image, imageInfo.RepoDigests) + if clientErr != nil { + logger.Error("failed to compare digest", zap.Error(clientErr)) + return false, clientErr + } + // match means no update available + return !match, nil + } + storeTag, err := storeComposeApp.MainTag() + return currentTag != storeTag, err +} + +func (a *AppStoreManagement) IsUpdating(appID string) bool { + _, ok := a.isAppUpgrading.Load(appID) + return ok +} + +func (a *AppStoreManagement) StartUpgrade(appID string) { + a.isAppUpgrading.Store(appID, struct{}{}) +} + +func (a *AppStoreManagement) FinishUpgrade(appID string) { + a.isAppUpgrading.Delete(appID) +} + func NewAppStoreManagement() *AppStoreManagement { defaultAppStore, err := NewDefaultAppStore() if err != nil { @@ -416,6 +530,8 @@ func NewAppStoreManagement() *AppStoreManagement { appStoreManagement := &AppStoreManagement{ defaultAppStore: defaultAppStore, + isAppUpgradable: gcache.New(100).LRU().Expiration(1 * time.Hour).Build(), + isAppUpgrading: sync.Map{}, } return appStoreManagement diff --git a/service/appstore_management_test.go b/service/appstore_management_test.go index b6426a1e..44a1b279 100644 --- a/service/appstore_management_test.go +++ b/service/appstore_management_test.go @@ -2,6 +2,7 @@ package service_test import ( "os" + "path/filepath" "runtime" "strings" "testing" @@ -11,9 +12,11 @@ import ( "github.com/IceWhaleTech/CasaOS-AppManagement/pkg/config" "github.com/IceWhaleTech/CasaOS-AppManagement/pkg/docker" "github.com/IceWhaleTech/CasaOS-AppManagement/service" + "github.com/IceWhaleTech/CasaOS-Common/utils/file" "github.com/IceWhaleTech/CasaOS-Common/utils/logger" "go.uber.org/goleak" "golang.org/x/net/context" + "gopkg.in/yaml.v3" "gotest.tools/v3/assert" ) @@ -83,3 +86,52 @@ func TestAppStoreList(t *testing.T) { assert.DeepEqual(t, registeredAppStoreList, unregisteredAppStoreList) } + +func TestIsUpgradable(t *testing.T) { + defer goleak.VerifyNone(t, goleak.IgnoreTopFunction(topFunc1), goleak.IgnoreTopFunction(pollFunc1), goleak.IgnoreTopFunction(httpFunc1)) // https://github.com/census-instrumentation/opencensus-go/issues/1191 + + defer func() { + // workaround due to https://github.com/patrickmn/go-cache/issues/166 + docker.Cache = nil + runtime.GC() + }() + + logger.LogInitConsoleOnly() + + appStoreManagement := service.NewAppStoreManagement() + + // mock store compose app + storeComposeApp, err := service.NewComposeAppFromYAML([]byte(common.SampleComposeAppYAML), true, false) + assert.NilError(t, err) + + storeComposeApp.SetStoreAppID("test") + + storeMainAppImage, _ := docker.ExtractImageAndTag(storeComposeApp.Services[0].Image) + + storeComposeAppStoreInfo, err := storeComposeApp.StoreInfo(false) + assert.NilError(t, err) + + // mock local compose app + appsPath := t.TempDir() + + composeFilePath := filepath.Join(appsPath, common.ComposeYAMLFileName) + + buf, err := yaml.Marshal(storeComposeApp) + assert.NilError(t, err) + + err = file.WriteToFullPath(buf, composeFilePath, 0o644) + assert.NilError(t, err) + + localComposeApp, err := service.LoadComposeAppFromConfigFile(*storeComposeAppStoreInfo.StoreAppID, composeFilePath) + assert.NilError(t, err) + + upgradable, err := appStoreManagement.IsUpdateAvailableWith(localComposeApp, storeComposeApp) + assert.NilError(t, err) + assert.Assert(t, !upgradable) + + storeComposeApp.Services[0].Image = storeMainAppImage + ":test" + + upgradable, err = appStoreManagement.IsUpdateAvailableWith(localComposeApp, storeComposeApp) + assert.NilError(t, err) + assert.Assert(t, upgradable) +} diff --git a/service/appstore_test.go b/service/appstore_test.go index 95e73f97..3d9ac7ad 100644 --- a/service/appstore_test.go +++ b/service/appstore_test.go @@ -90,12 +90,12 @@ func TestGetApp(t *testing.T) { func TestSkipUpdateCatalog(t *testing.T) { logger.LogInitConsoleOnly() - appStoreUrl := []string{ + appStoreURL := []string{ "https://casaos.app/store/main.zip", "https://casaos.oss-cn-shanghai.aliyuncs.com/store/main.zip", } - for _, url := range appStoreUrl { + for _, url := range appStoreURL { appStore, err := service.AppStoreByURL(url) assert.NilError(t, err) workdir, err := appStore.WorkDir() @@ -112,19 +112,19 @@ func TestSkipUpdateCatalog(t *testing.T) { assert.NilError(t, err) // get create and change time of appstore - appStoreStat_first, err := os.Stat(workdir) + appStoreStatFirst, err := os.Stat(workdir) assert.NilError(t, err) - assert.Equal(t, false, appStoreStat_first.ModTime().Equal(appStoreStat.ModTime())) + assert.Equal(t, false, appStoreStatFirst.ModTime().Equal(appStoreStat.ModTime())) err = appStore.UpdateCatalog() assert.NilError(t, err) // get create and change time of appstore - appStoreStat_second, err := os.Stat(workdir) + appStoreStatSecond, err := os.Stat(workdir) assert.NilError(t, err) - assert.Equal(t, appStoreStat_first.ModTime(), appStoreStat_second.ModTime()) + assert.Equal(t, appStoreStatFirst.ModTime(), appStoreStatSecond.ModTime()) } } diff --git a/service/compose_app.go b/service/compose_app.go index a5828027..70ac91e4 100644 --- a/service/compose_app.go +++ b/service/compose_app.go @@ -14,7 +14,6 @@ import ( "time" v1 "github.com/IceWhaleTech/CasaOS-AppManagement/service/v1" - "github.com/bluele/gcache" "github.com/IceWhaleTech/CasaOS-AppManagement/codegen" "github.com/IceWhaleTech/CasaOS-AppManagement/common" @@ -30,7 +29,6 @@ import ( "github.com/compose-spec/compose-go/loader" "github.com/compose-spec/compose-go/types" composeCmd "github.com/docker/compose/v2/cmd/compose" - "github.com/docker/docker/client" "github.com/docker/compose/v2/cmd/formatter" "github.com/docker/compose/v2/pkg/api" @@ -164,116 +162,6 @@ func (a *ComposeApp) SetTitle(title, lang string) { } } -func (a *ComposeApp) IsUpdateAvailable() bool { - storeInfo, err := a.StoreInfo(false) - if err != nil { - logger.Error("failed to get store info of compose app, thus no update available", zap.Error(err)) - return false - } - - if storeInfo == nil || storeInfo.StoreAppID == nil || *storeInfo.StoreAppID == "" { - logger.Error("store info of compose app is not valid, thus no update available") - return false - } - - storeComposeApp, err := MyService.V2AppStore().ComposeApp(*storeInfo.StoreAppID) - if err != nil { - logger.Error("failed to get store compose app, thus no update available", zap.Error(err)) - return false - } - - if storeComposeApp == nil { - logger.Error("store compose app not found, thus no update available", zap.String("storeAppID", *storeInfo.StoreAppID)) - return false - } - - return a.IsUpdateAvailableWith(storeComposeApp) -} - -var latestIsUpdateAvailableCache = gcache.New(100). - LRU(). - Expiration(1 * time.Hour). - Build() - -func (a *ComposeApp) IsUpdateAvailableWith(storeComposeApp *ComposeApp) bool { - storeComposeAppStoreInfo, err := storeComposeApp.StoreInfo(false) - if err != nil || storeComposeAppStoreInfo == nil { - logger.Error("failed to get store info of store compose app, thus no update available", zap.Error(err)) - return false - } - - mainAppName := *storeComposeAppStoreInfo.Main - - mainApp := a.App(mainAppName) - if mainApp == nil { - logger.Error("main app not found in local compose app, thus no update available", zap.String("name", mainAppName)) - return false - } - - mainAppImage, mainAppTag := docker.ExtractImageAndTag(mainApp.Image) - - // TODO to async the check for consist with the version tag app - if mainAppTag == "latest" { - isUpdateAvailable, err := latestIsUpdateAvailableCache.Get(mainAppImage) - if err != nil { - go func() { - // check image digest when tag is latest. - ctx := context.Background() - cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) - if err != nil { - logger.Error("failed to create docker client", zap.Error(err)) - } - defer cli.Close() - - imageInfo, _, err := cli.ImageInspectWithRaw(ctx, mainAppImage) - - if err != nil { - logger.Error("failed to inspect image", zap.Error(err), zap.String("name", mainAppImage)) - latestIsUpdateAvailableCache.Set(mainAppImage, false) - } - match, err := docker.CompareDigest(storeComposeApp.Services[0].Image, imageInfo.RepoDigests) - if err != nil { - logger.Error("failed to compare digest", zap.Error(err), zap.String("name", mainAppImage)) - latestIsUpdateAvailableCache.Set(mainAppImage, false) - } - if match { - logger.Info("main app image tag is latest, thus no update available", zap.String("image", mainApp.Image)) - latestIsUpdateAvailableCache.Set(mainAppImage, false) - } else { - logger.Info("main app image tag is latest, but digest is different, thus update is available", zap.String("image", mainApp.Image)) - latestIsUpdateAvailableCache.Set(mainAppImage, true) - } - }() - - return false - } - if isUpdateAvailable.(bool) { - return true - } - return false - } - - storeMainApp := storeComposeApp.App(mainAppName) - if storeMainApp == nil { - logger.Error("main app not found in store compose app, thus no update available", zap.String("name", mainAppName)) - return false - } - - storeMainAppImage, storeMainAppTag := docker.ExtractImageAndTag(storeMainApp.Image) - - if mainAppImage != storeMainAppImage { - logger.Error("main app image not match for local app and store app, thus no update available", zap.String("local", mainApp.Image), zap.String("store", storeMainApp.Image)) - return false - } - - if mainAppTag == storeMainAppTag { - return false - } - - logger.Info("main apps of local app and store app have different image tag, thus update is available", zap.String("local", mainApp.Image), zap.String("store", storeMainApp.Image)) - return true -} - func (a *ComposeApp) Update(ctx context.Context) error { if len(a.ComposeFiles) <= 0 { return ErrComposeFileNotFound diff --git a/service/compose_app_test.go b/service/compose_app_test.go index 62b93d5a..ff676ca3 100644 --- a/service/compose_app_test.go +++ b/service/compose_app_test.go @@ -2,17 +2,14 @@ package service_test import ( "encoding/json" - "path/filepath" "runtime" "testing" "github.com/IceWhaleTech/CasaOS-AppManagement/common" "github.com/IceWhaleTech/CasaOS-AppManagement/pkg/docker" "github.com/IceWhaleTech/CasaOS-AppManagement/service" - "github.com/IceWhaleTech/CasaOS-Common/utils/file" "github.com/IceWhaleTech/CasaOS-Common/utils/logger" "go.uber.org/goleak" - "gopkg.in/yaml.v3" "gotest.tools/v3/assert" ) @@ -57,51 +54,6 @@ func TestUpdateEventPropertiesFromStoreInfo(t *testing.T) { assert.Equal(t, title, storeInfo.Title[common.DefaultLanguage]) } -func TestIsUpgradable(t *testing.T) { - defer goleak.VerifyNone(t, goleak.IgnoreTopFunction(topFunc1), goleak.IgnoreTopFunction(pollFunc1), goleak.IgnoreTopFunction(httpFunc1)) // https://github.com/census-instrumentation/opencensus-go/issues/1191 - - defer func() { - // workaround due to https://github.com/patrickmn/go-cache/issues/166 - docker.Cache = nil - runtime.GC() - }() - - logger.LogInitConsoleOnly() - - // mock store compose app - storeComposeApp, err := service.NewComposeAppFromYAML([]byte(common.SampleComposeAppYAML), true, false) - assert.NilError(t, err) - - storeComposeApp.SetStoreAppID("test") - - storeMainAppImage, _ := docker.ExtractImageAndTag(storeComposeApp.Services[0].Image) - - storeComposeAppStoreInfo, err := storeComposeApp.StoreInfo(false) - assert.NilError(t, err) - - // mock local compose app - appsPath := t.TempDir() - - composeFilePath := filepath.Join(appsPath, common.ComposeYAMLFileName) - - buf, err := yaml.Marshal(storeComposeApp) - assert.NilError(t, err) - - err = file.WriteToFullPath(buf, composeFilePath, 0o644) - assert.NilError(t, err) - - localComposeApp, err := service.LoadComposeAppFromConfigFile(*storeComposeAppStoreInfo.StoreAppID, composeFilePath) - assert.NilError(t, err) - - upgradable := localComposeApp.IsUpdateAvailableWith(storeComposeApp) - assert.Assert(t, !upgradable) - - storeComposeApp.Services[0].Image = storeMainAppImage + ":test" - - upgradable = localComposeApp.IsUpdateAvailableWith(storeComposeApp) - assert.Assert(t, upgradable) -} - func TestNameAndTitle(t *testing.T) { defer goleak.VerifyNone(t, goleak.IgnoreTopFunction(topFunc1), goleak.IgnoreTopFunction(pollFunc1), goleak.IgnoreTopFunction(httpFunc1)) // https://github.com/census-instrumentation/opencensus-go/issues/1191