Skip to content

Commit

Permalink
add upgradable app api (#182)
Browse files Browse the repository at this point in the history
  • Loading branch information
CorrectRoadH committed Apr 11, 2024
1 parent 1c89f7f commit aeb479e
Show file tree
Hide file tree
Showing 10 changed files with 228 additions and 186 deletions.
4 changes: 4 additions & 0 deletions api/app_management/openapi.yaml
Expand Up @@ -1480,6 +1480,7 @@ components:
- version
- store_app_id
- status
- icon
properties:
title:
type: string
Expand All @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions cmd/message-bus-docgen/main.go
Expand Up @@ -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,
Expand Down
7 changes: 3 additions & 4 deletions cmd/migration-tool/log.go
Expand Up @@ -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 {
Expand Down
44 changes: 36 additions & 8 deletions route/v2/appstore.go
Expand Up @@ -2,6 +2,7 @@ package v2

import (
"context"
"encoding/json"
"fmt"
"net/http"
"path/filepath"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
})
}
9 changes: 6 additions & 3 deletions route/v2/compose_app.go
Expand Up @@ -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{
Expand Down Expand Up @@ -453,14 +453,17 @@ 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})
}
}

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()
Expand Down Expand Up @@ -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

Expand Down
122 changes: 119 additions & 3 deletions service/appstore_management.go
Expand Up @@ -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"
)
Expand All @@ -19,6 +24,8 @@ type AppStoreManagement struct {
onAppStoreRegister []func(string) error
onAppStoreUnregister []func(string) error

isAppUpgradable gcache.Cache
isAppUpgrading sync.Map
defaultAppStore AppStore
}

Expand Down Expand Up @@ -366,6 +373,9 @@ func (a *AppStoreManagement) UpdateCatalog() error {
}
}

// clean cache
a.isAppUpgradable.Purge()

return nil
}

Expand All @@ -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
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down

0 comments on commit aeb479e

Please sign in to comment.