feat(appstore): use shared resolver
This commit is contained in:
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
## Pending
|
## Pending
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
- switch App Store APIs to the shared appstore client
|
||||||
|
- Replaces Cloudly App Store manager naming and request methods with App Store naming
|
||||||
|
- Uses `@serve.zone/appstore` for app metadata parsing and source resolution
|
||||||
|
- Adds `servezone.appstore.json` as Cloudly's source-owned App Store manifest
|
||||||
|
|
||||||
## 2026-05-24 - 5.9.0
|
## 2026-05-24 - 5.9.0
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -79,7 +79,8 @@
|
|||||||
"@push.rocks/taskbuffer": "^8.0.2",
|
"@push.rocks/taskbuffer": "^8.0.2",
|
||||||
"@push.rocks/webjwt": "^1.0.10",
|
"@push.rocks/webjwt": "^1.0.10",
|
||||||
"@serve.zone/api": "^5.3.8",
|
"@serve.zone/api": "^5.3.8",
|
||||||
"@serve.zone/interfaces": "^5.10.0",
|
"@serve.zone/appstore": "^0.2.0",
|
||||||
|
"@serve.zone/interfaces": "^6.0.0",
|
||||||
"@tsclass/tsclass": "^9.5.1"
|
"@tsclass/tsclass": "^9.5.1"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
Generated
+16
-6
@@ -143,9 +143,12 @@ importers:
|
|||||||
'@serve.zone/api':
|
'@serve.zone/api':
|
||||||
specifier: ^5.3.8
|
specifier: ^5.3.8
|
||||||
version: 5.3.8(@push.rocks/smartserve@2.0.4)
|
version: 5.3.8(@push.rocks/smartserve@2.0.4)
|
||||||
|
'@serve.zone/appstore':
|
||||||
|
specifier: ^0.2.0
|
||||||
|
version: 0.2.0
|
||||||
'@serve.zone/interfaces':
|
'@serve.zone/interfaces':
|
||||||
specifier: ^5.10.0
|
specifier: ^6.0.0
|
||||||
version: 5.10.0
|
version: 6.0.0
|
||||||
'@tsclass/tsclass':
|
'@tsclass/tsclass':
|
||||||
specifier: ^9.5.1
|
specifier: ^9.5.1
|
||||||
version: 9.5.1
|
version: 9.5.1
|
||||||
@@ -2030,11 +2033,14 @@ packages:
|
|||||||
'@serve.zone/api@5.3.8':
|
'@serve.zone/api@5.3.8':
|
||||||
resolution: {integrity: sha512-k3IU4mcHuk5pKB+X7rhYWGK+j5hyyDzFoqR3ytzG1iidvgDEIIToQJq+mB3E1v6X1+tI3WyYUaMN/TaZRz0l0w==}
|
resolution: {integrity: sha512-k3IU4mcHuk5pKB+X7rhYWGK+j5hyyDzFoqR3ytzG1iidvgDEIIToQJq+mB3E1v6X1+tI3WyYUaMN/TaZRz0l0w==}
|
||||||
|
|
||||||
|
'@serve.zone/appstore@0.2.0':
|
||||||
|
resolution: {integrity: sha512-qt2LVaRpzfJdUywllm+F0njwnN3aHc2aZHEcjc9REn1VDT47UuUEGaKkfNiosGK0GJqb1hPI/GwyuGMe4H4q7w==}
|
||||||
|
|
||||||
'@serve.zone/interfaces@5.10.0':
|
'@serve.zone/interfaces@5.10.0':
|
||||||
resolution: {integrity: sha512-8ZnP1A43UZlYwfd2j+S0Yin//didacIX2Rou9MobRuSFFgi1RQOqQcIWqOINcDk80wBDuYkyMCwHygYxD5i+Ig==}
|
resolution: {integrity: sha512-8ZnP1A43UZlYwfd2j+S0Yin//didacIX2Rou9MobRuSFFgi1RQOqQcIWqOINcDk80wBDuYkyMCwHygYxD5i+Ig==}
|
||||||
|
|
||||||
'@serve.zone/interfaces@5.9.0':
|
'@serve.zone/interfaces@6.0.0':
|
||||||
resolution: {integrity: sha512-XMXyTXTMcB8AX6zYOMO+Jt5bOv9ujyXj5miE6lrgyT8g+eJ/I6sUFqVNUKJ3LiMk/yFWsPln7HtZeZKDEhaCwQ==}
|
resolution: {integrity: sha512-nCidhOH0XlX+7e6xaJDq6fwnwaWasB/4w2LHkV7A96G+m+7EXZqbbaKSboUlaiGDly0dWNajk2FrYFo64ZucPA==}
|
||||||
|
|
||||||
'@shikijs/engine-oniguruma@3.23.0':
|
'@shikijs/engine-oniguruma@3.23.0':
|
||||||
resolution: {integrity: sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==}
|
resolution: {integrity: sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==}
|
||||||
@@ -7774,18 +7780,22 @@ snapshots:
|
|||||||
'@push.rocks/smartpromise': 4.2.4
|
'@push.rocks/smartpromise': 4.2.4
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
'@push.rocks/smartstream': 3.4.2
|
'@push.rocks/smartstream': 3.4.2
|
||||||
'@serve.zone/interfaces': 5.9.0
|
'@serve.zone/interfaces': 5.10.0
|
||||||
'@tsclass/tsclass': 9.5.1
|
'@tsclass/tsclass': 9.5.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@push.rocks/smartserve'
|
- '@push.rocks/smartserve'
|
||||||
|
|
||||||
|
'@serve.zone/appstore@0.2.0':
|
||||||
|
dependencies:
|
||||||
|
'@serve.zone/interfaces': 6.0.0
|
||||||
|
|
||||||
'@serve.zone/interfaces@5.10.0':
|
'@serve.zone/interfaces@5.10.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedrequest-interfaces': 3.0.19
|
'@api.global/typedrequest-interfaces': 3.0.19
|
||||||
'@push.rocks/smartlog-interfaces': 3.0.2
|
'@push.rocks/smartlog-interfaces': 3.0.2
|
||||||
'@tsclass/tsclass': 9.5.1
|
'@tsclass/tsclass': 9.5.1
|
||||||
|
|
||||||
'@serve.zone/interfaces@5.9.0':
|
'@serve.zone/interfaces@6.0.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedrequest-interfaces': 3.0.19
|
'@api.global/typedrequest-interfaces': 3.0.19
|
||||||
'@push.rocks/smartlog-interfaces': 3.0.2
|
'@push.rocks/smartlog-interfaces': 3.0.2
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
minimumReleaseAgeExclude:
|
minimumReleaseAgeExclude:
|
||||||
- '@serve.zone/api'
|
- '@serve.zone/api'
|
||||||
|
- '@serve.zone/appstore'
|
||||||
- '@serve.zone/interfaces'
|
- '@serve.zone/interfaces'
|
||||||
|
|
||||||
allowBuilds:
|
allowBuilds:
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"app": {
|
||||||
|
"id": "cloudly",
|
||||||
|
"name": "Cloudly",
|
||||||
|
"description": "Multi-node serve.zone control plane for clusters, workload services, domains, and deployments.",
|
||||||
|
"category": "Dev Tools",
|
||||||
|
"iconName": "server",
|
||||||
|
"tags": ["serve.zone", "control-plane", "clusters", "deployments"],
|
||||||
|
"maintainer": "serve.zone",
|
||||||
|
"links": {
|
||||||
|
"Source": "https://code.foss.global/serve.zone/cloudly",
|
||||||
|
"Docs": "https://serve.zone"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"latestVersion": "latest",
|
||||||
|
"source": {
|
||||||
|
"type": "dockerImage",
|
||||||
|
"image": "code.foss.global/serve.zone/cloudly:latest",
|
||||||
|
"tracking": "digest"
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"image": "code.foss.global/serve.zone/cloudly:latest",
|
||||||
|
"port": 80,
|
||||||
|
"envVars": [
|
||||||
|
{
|
||||||
|
"key": "SERVEZONE_ENVIRONMENT",
|
||||||
|
"value": "production",
|
||||||
|
"description": "Cloudly runtime environment.",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "SERVEZONE_URL",
|
||||||
|
"value": "${SERVICE_DOMAIN}",
|
||||||
|
"description": "Public Cloudly hostname without protocol.",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "SERVEZONE_PORT",
|
||||||
|
"value": "80",
|
||||||
|
"description": "Internal Cloudly HTTP port inside the container.",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "SERVEZONE_SSLMODE",
|
||||||
|
"value": "external",
|
||||||
|
"description": "Use external TLS termination through Onebox or dcrouter.",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "SERVEZONE_ADMINACCOUNT",
|
||||||
|
"value": "",
|
||||||
|
"description": "Initial admin account in username:password format. Only used when Cloudly has no human users yet.",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "MONGODB_URL",
|
||||||
|
"value": "${MONGODB_URI}",
|
||||||
|
"description": "Authenticated MongoDB connection URL provisioned by Onebox.",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "MONGODB_NAME",
|
||||||
|
"value": "${MONGODB_DATABASE}",
|
||||||
|
"description": "MongoDB database name provisioned by Onebox.",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "MONGODB_USER",
|
||||||
|
"value": "${MONGODB_USERNAME}",
|
||||||
|
"description": "MongoDB username provisioned by Onebox.",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "MONGODB_PASS",
|
||||||
|
"value": "${MONGODB_PASSWORD}",
|
||||||
|
"description": "MongoDB password provisioned by Onebox.",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "S3_ENDPOINT",
|
||||||
|
"value": "onebox-minio",
|
||||||
|
"description": "S3 endpoint host for the MinIO service provisioned by Onebox.",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "S3_PORT",
|
||||||
|
"value": "9000",
|
||||||
|
"description": "S3 endpoint port for the MinIO service provisioned by Onebox.",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "S3_USESSL",
|
||||||
|
"value": "false",
|
||||||
|
"description": "Use plain HTTP for internal MinIO traffic on the Onebox network.",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "S3_BUCKET",
|
||||||
|
"value": "${S3_BUCKET}",
|
||||||
|
"description": "S3 bucket provisioned by Onebox for Cloudly's registry.",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "S3_ACCESSKEY",
|
||||||
|
"value": "${S3_ACCESS_KEY}",
|
||||||
|
"description": "S3 access key provisioned by Onebox.",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "S3_SECRETKEY",
|
||||||
|
"value": "${S3_SECRET_KEY}",
|
||||||
|
"description": "S3 secret key provisioned by Onebox.",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"platformRequirements": {
|
||||||
|
"mongodb": true,
|
||||||
|
"s3": true
|
||||||
|
},
|
||||||
|
"minOneboxVersion": "1.24.2",
|
||||||
|
"backupBeforeUpgrade": true,
|
||||||
|
"healthCheck": {
|
||||||
|
"path": "/status",
|
||||||
|
"port": 80,
|
||||||
|
"expectedStatus": 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ import { CloudlySettingsManager } from './manager.settings/classes.settingsmanag
|
|||||||
import { CloudlyPlatformManager } from './manager.platform/classes.platformmanager.js';
|
import { CloudlyPlatformManager } from './manager.platform/classes.platformmanager.js';
|
||||||
import { CloudlyBackupManager } from './manager.backup/classes.backupmanager.js';
|
import { CloudlyBackupManager } from './manager.backup/classes.backupmanager.js';
|
||||||
import { CloudlyBaseOsManager } from './manager.baseos/classes.baseosmanager.js';
|
import { CloudlyBaseOsManager } from './manager.baseos/classes.baseosmanager.js';
|
||||||
import { CloudlyAppCatalogManager } from './manager.appcatalog/classes.appcatalogmanager.js';
|
import { CloudlyAppStoreManager } from './manager.appstore/classes.appstoremanager.js';
|
||||||
import { CloudlyJumpManager } from './manager.jump/classes.jumpmanager.js';
|
import { CloudlyJumpManager } from './manager.jump/classes.jumpmanager.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,7 +81,7 @@ export class Cloudly {
|
|||||||
public nodeManager: CloudlyNodeManager;
|
public nodeManager: CloudlyNodeManager;
|
||||||
public baremetalManager: CloudlyBaremetalManager;
|
public baremetalManager: CloudlyBaremetalManager;
|
||||||
public baseOsManager: CloudlyBaseOsManager;
|
public baseOsManager: CloudlyBaseOsManager;
|
||||||
public appCatalogManager: CloudlyAppCatalogManager;
|
public appStoreManager: CloudlyAppStoreManager;
|
||||||
public jumpManager: CloudlyJumpManager;
|
public jumpManager: CloudlyJumpManager;
|
||||||
|
|
||||||
private readyDeferred = new plugins.smartpromise.Deferred();
|
private readyDeferred = new plugins.smartpromise.Deferred();
|
||||||
@@ -119,7 +119,7 @@ export class Cloudly {
|
|||||||
this.backupManager = new CloudlyBackupManager(this);
|
this.backupManager = new CloudlyBackupManager(this);
|
||||||
this.baseOsManager = new CloudlyBaseOsManager(this);
|
this.baseOsManager = new CloudlyBaseOsManager(this);
|
||||||
this.secretManager = new CloudlySecretManager(this);
|
this.secretManager = new CloudlySecretManager(this);
|
||||||
this.appCatalogManager = new CloudlyAppCatalogManager(this);
|
this.appStoreManager = new CloudlyAppStoreManager(this);
|
||||||
this.nodeManager = new CloudlyNodeManager(this);
|
this.nodeManager = new CloudlyNodeManager(this);
|
||||||
this.baremetalManager = new CloudlyBaremetalManager(this);
|
this.baremetalManager = new CloudlyBaremetalManager(this);
|
||||||
this.jumpManager = new CloudlyJumpManager(this);
|
this.jumpManager = new CloudlyJumpManager(this);
|
||||||
@@ -151,7 +151,7 @@ export class Cloudly {
|
|||||||
await this.taskManager.init();
|
await this.taskManager.init();
|
||||||
await this.backupManager.start();
|
await this.backupManager.start();
|
||||||
await this.baseOsManager.start();
|
await this.baseOsManager.start();
|
||||||
await this.appCatalogManager.start();
|
await this.appStoreManager.start();
|
||||||
await this.registryManager.start();
|
await this.registryManager.start();
|
||||||
await this.domainManager.init();
|
await this.domainManager.init();
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ export class Cloudly {
|
|||||||
await this.backupManager.stop();
|
await this.backupManager.stop();
|
||||||
await this.baseOsManager.stop();
|
await this.baseOsManager.stop();
|
||||||
await this.registryManager.stop();
|
await this.registryManager.stop();
|
||||||
await this.appCatalogManager.stop();
|
await this.appStoreManager.stop();
|
||||||
await this.externalRegistryManager.stop();
|
await this.externalRegistryManager.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+68
-91
@@ -5,19 +5,18 @@ import { Service } from '../manager.service/classes.service.js';
|
|||||||
import { SecretBundle } from '../manager.secret/classes.secretbundle.js';
|
import { SecretBundle } from '../manager.secret/classes.secretbundle.js';
|
||||||
import { PlatformBinding } from '../manager.platform/classes.platformbinding.js';
|
import { PlatformBinding } from '../manager.platform/classes.platformbinding.js';
|
||||||
|
|
||||||
type ICatalogApp = plugins.servezoneInterfaces.appcatalog.ICatalogApp;
|
type IAppStoreApp = plugins.servezoneInterfaces.appstore.IAppStoreApp;
|
||||||
type ICatalog = plugins.servezoneInterfaces.appcatalog.ICatalog;
|
type IAppStoreIndex = plugins.servezoneInterfaces.appstore.IAppStoreIndex;
|
||||||
type IAppMeta = plugins.servezoneInterfaces.appcatalog.IAppMeta;
|
type IAppStoreAppMeta = plugins.servezoneInterfaces.appstore.IAppStoreAppMeta;
|
||||||
type IAppVersionConfig = plugins.servezoneInterfaces.appcatalog.IAppVersionConfig;
|
type IAppStoreVersionConfig = plugins.servezoneInterfaces.appstore.IAppStoreVersionConfig;
|
||||||
type IInstallOptions = plugins.servezoneInterfaces.appcatalog.IAppInstallRequest;
|
type IAppStoreInstallOptions = plugins.servezoneInterfaces.appstore.IAppStoreInstallRequest;
|
||||||
type IUpgradeableCatalogService = plugins.servezoneInterfaces.appcatalog.IUpgradeableAppService;
|
type IUpgradeableAppStoreService = plugins.servezoneInterfaces.appstore.IUpgradeableAppStoreService;
|
||||||
|
|
||||||
export class CloudlyAppCatalogManager {
|
export class CloudlyAppStoreManager {
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
private catalogCache: ICatalog | null = null;
|
private readonly appStoreResolver = new plugins.servezoneAppstore.AppStoreResolver({
|
||||||
private lastFetchTime = 0;
|
baseUrl: process.env.APPSTORE_URL || 'https://code.foss.global/serve.zone/appstore/raw/branch/main',
|
||||||
private readonly repoBaseUrl = process.env.APPCATALOG_URL || 'https://code.foss.global/serve.zone/appstore-apptemplates/raw/branch/main';
|
});
|
||||||
private readonly cacheTtlMs = 5 * 60 * 1000;
|
|
||||||
|
|
||||||
constructor(private cloudlyRef: Cloudly) {
|
constructor(private cloudlyRef: Cloudly) {
|
||||||
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
|
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
@@ -28,88 +27,74 @@ export class CloudlyAppCatalogManager {
|
|||||||
public async stop() {}
|
public async stop() {}
|
||||||
|
|
||||||
private registerHandlers() {
|
private registerHandlers() {
|
||||||
const addCatalogListHandler = (methodArg: string) => {
|
this.typedrouter.addTypedHandler(
|
||||||
this.typedrouter.addTypedHandler(
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.appstore.IReq_Any_GetAppStoreTemplates>(
|
||||||
new plugins.typedrequest.TypedHandler<any>(methodArg, async (dataArg) => {
|
'getAppStoreTemplates',
|
||||||
|
async (dataArg) => {
|
||||||
await this.passAdminIdentity(dataArg);
|
await this.passAdminIdentity(dataArg);
|
||||||
return { apps: await this.getApps() };
|
return { apps: await this.getApps() };
|
||||||
}),
|
},
|
||||||
);
|
),
|
||||||
};
|
);
|
||||||
addCatalogListHandler('getAppCatalogTemplates');
|
|
||||||
addCatalogListHandler('getAppTemplates');
|
|
||||||
|
|
||||||
const addConfigHandler = (methodArg: string) => {
|
this.typedrouter.addTypedHandler(
|
||||||
this.typedrouter.addTypedHandler(
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.appstore.IReq_Any_GetAppStoreConfig>(
|
||||||
new plugins.typedrequest.TypedHandler<any>(methodArg, async (dataArg) => {
|
'getAppStoreConfig',
|
||||||
|
async (dataArg) => {
|
||||||
await this.passAdminIdentity(dataArg);
|
await this.passAdminIdentity(dataArg);
|
||||||
return {
|
return {
|
||||||
config: await this.getAppVersionConfig(dataArg.appId, dataArg.version),
|
config: await this.getAppVersionConfig(dataArg.appId, dataArg.version),
|
||||||
appMeta: await this.getAppMeta(dataArg.appId),
|
appMeta: await this.getAppMeta(dataArg.appId),
|
||||||
};
|
};
|
||||||
}),
|
},
|
||||||
);
|
),
|
||||||
};
|
);
|
||||||
addConfigHandler('getAppCatalogConfig');
|
|
||||||
addConfigHandler('getAppConfig');
|
|
||||||
|
|
||||||
const addInstallHandler = (methodArg: string) => {
|
this.typedrouter.addTypedHandler(
|
||||||
this.typedrouter.addTypedHandler(
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.appstore.IReq_Any_InstallAppStoreApp>(
|
||||||
new plugins.typedrequest.TypedHandler<any>(methodArg, async (dataArg) => {
|
'installAppStoreApp',
|
||||||
|
async (dataArg) => {
|
||||||
await this.passAdminIdentity(dataArg);
|
await this.passAdminIdentity(dataArg);
|
||||||
const service = await this.installApp(dataArg.install || dataArg);
|
const service = await this.installApp(dataArg.install);
|
||||||
return { service: await service.createSavableObject() };
|
return { service: await service.createSavableObject() };
|
||||||
}),
|
},
|
||||||
);
|
),
|
||||||
};
|
);
|
||||||
addInstallHandler('installAppCatalogApp');
|
|
||||||
addInstallHandler('installAppTemplate');
|
|
||||||
|
|
||||||
const addUpgradeableHandler = (methodArg: string) => {
|
this.typedrouter.addTypedHandler(
|
||||||
this.typedrouter.addTypedHandler(
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.appstore.IReq_Any_GetUpgradeableAppStoreServices>(
|
||||||
new plugins.typedrequest.TypedHandler<any>(methodArg, async (dataArg) => {
|
'getUpgradeableAppStoreServices',
|
||||||
|
async (dataArg) => {
|
||||||
await this.passAdminIdentity(dataArg);
|
await this.passAdminIdentity(dataArg);
|
||||||
return { services: await this.getUpgradeableServices() };
|
return { services: await this.getUpgradeableAppStoreServices() };
|
||||||
}),
|
},
|
||||||
);
|
),
|
||||||
};
|
);
|
||||||
addUpgradeableHandler('getUpgradeableAppCatalogServices');
|
|
||||||
addUpgradeableHandler('getUpgradeableServices');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getCatalog(): Promise<ICatalog> {
|
public async getAppStore(): Promise<IAppStoreIndex> {
|
||||||
const now = Date.now();
|
return await this.appStoreResolver.getAppStoreIndex();
|
||||||
if (this.catalogCache && now - this.lastFetchTime < this.cacheTtlMs) {
|
|
||||||
return this.catalogCache;
|
|
||||||
}
|
|
||||||
const catalog = await this.fetchJson('catalog.json') as ICatalog;
|
|
||||||
if (!catalog || !Array.isArray(catalog.apps)) {
|
|
||||||
throw new Error('Invalid app catalog format');
|
|
||||||
}
|
|
||||||
this.catalogCache = catalog;
|
|
||||||
this.lastFetchTime = now;
|
|
||||||
return catalog;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getApps(): Promise<ICatalogApp[]> {
|
public async getApps(): Promise<IAppStoreApp[]> {
|
||||||
return (await this.getCatalog()).apps;
|
return await this.appStoreResolver.getApps();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAppMeta(appIdArg: string): Promise<IAppMeta> {
|
public async getAppMeta(appIdArg: string): Promise<IAppStoreAppMeta> {
|
||||||
return await this.fetchJson(`apps/${appIdArg}/app.json`) as IAppMeta;
|
return await this.appStoreResolver.getAppMeta(appIdArg);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAppVersionConfig(appIdArg: string, versionArg?: string): Promise<IAppVersionConfig> {
|
public async getAppVersionConfig(appIdArg: string, versionArg?: string): Promise<IAppStoreVersionConfig> {
|
||||||
if (!versionArg) {
|
if (!versionArg) {
|
||||||
versionArg = (await this.getAppMeta(appIdArg)).latestVersion;
|
versionArg = (await this.getAppMeta(appIdArg)).latestVersion;
|
||||||
}
|
}
|
||||||
return await this.fetchJson(`apps/${appIdArg}/versions/${versionArg}/config.json`) as IAppVersionConfig;
|
return await this.appStoreResolver.getAppVersionConfig(appIdArg, versionArg);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getUpgradeableServices(): Promise<IUpgradeableCatalogService[]> {
|
public async getUpgradeableAppStoreServices(): Promise<IUpgradeableAppStoreService[]> {
|
||||||
const catalog = await this.getCatalog();
|
const appStore = await this.getAppStore();
|
||||||
const services = await this.cloudlyRef.serviceManager.CService.getInstances({});
|
const services = await this.cloudlyRef.serviceManager.CService.getInstances({});
|
||||||
const upgradeableServices: IUpgradeableCatalogService[] = [];
|
const upgradeableServices: IUpgradeableAppStoreService[] = [];
|
||||||
|
|
||||||
for (const service of services) {
|
for (const service of services) {
|
||||||
const serviceData = service.data as plugins.servezoneInterfaces.data.IService['data'] & {
|
const serviceData = service.data as plugins.servezoneInterfaces.data.IService['data'] & {
|
||||||
@@ -119,15 +104,15 @@ export class CloudlyAppCatalogManager {
|
|||||||
if (!serviceData.appTemplateId || !serviceData.appTemplateVersion) {
|
if (!serviceData.appTemplateId || !serviceData.appTemplateVersion) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const catalogApp = catalog.apps.find((appArg) => appArg.id === serviceData.appTemplateId);
|
const appStoreApp = appStore.apps.find((appArg) => appArg.id === serviceData.appTemplateId);
|
||||||
if (!catalogApp || catalogApp.latestVersion === serviceData.appTemplateVersion) {
|
if (!appStoreApp || appStoreApp.latestVersion === serviceData.appTemplateVersion) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
upgradeableServices.push({
|
upgradeableServices.push({
|
||||||
serviceName: serviceData.name,
|
serviceName: serviceData.name,
|
||||||
appTemplateId: serviceData.appTemplateId,
|
appTemplateId: serviceData.appTemplateId,
|
||||||
currentVersion: serviceData.appTemplateVersion,
|
currentVersion: serviceData.appTemplateVersion,
|
||||||
latestVersion: catalogApp.latestVersion,
|
latestVersion: appStoreApp.latestVersion,
|
||||||
hasMigration: false,
|
hasMigration: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -135,18 +120,19 @@ export class CloudlyAppCatalogManager {
|
|||||||
return upgradeableServices;
|
return upgradeableServices;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async installApp(optionsArg: IInstallOptions): Promise<Service> {
|
public async installApp(optionsArg: IAppStoreInstallOptions): Promise<Service> {
|
||||||
const appMeta = await this.getAppMeta(optionsArg.appId);
|
const appMeta = await this.getAppMeta(optionsArg.appId);
|
||||||
const version = optionsArg.version || appMeta.latestVersion;
|
const version = optionsArg.version || appMeta.latestVersion;
|
||||||
const config = await this.getAppVersionConfig(optionsArg.appId, version);
|
const config = await this.getAppVersionConfig(optionsArg.appId, version);
|
||||||
|
const appStoreVersion = config.appStoreVersion || version;
|
||||||
const webPort = optionsArg.port || config.port;
|
const webPort = optionsArg.port || config.port;
|
||||||
this.assertSupportedPlatformRequirements(config);
|
this.assertSupportedPlatformRequirements(config);
|
||||||
const envVars = this.getCatalogEnvVars(config, optionsArg.envVars || {});
|
const envVars = this.getAppStoreEnvVars(config, optionsArg.envVars || {});
|
||||||
if (this.requiresTemplateValue(envVars, 'SERVICE_DOMAIN') && !optionsArg.domain) {
|
if (this.requiresTemplateValue(envVars, 'SERVICE_DOMAIN') && !optionsArg.domain) {
|
||||||
throw new Error('A domain is required because the app template uses ${SERVICE_DOMAIN}');
|
throw new Error('A domain is required because the app template uses ${SERVICE_DOMAIN}');
|
||||||
}
|
}
|
||||||
|
|
||||||
const image = await this.createCatalogImage(optionsArg.serviceName, config.image, appMeta.description);
|
const image = await this.createAppStoreImage(optionsArg.serviceName, config.image, appMeta.description);
|
||||||
const secretBundle = await this.createServiceSecretBundle(optionsArg.serviceName, image.id);
|
const secretBundle = await this.createServiceSecretBundle(optionsArg.serviceName, image.id);
|
||||||
const serviceData = {
|
const serviceData = {
|
||||||
name: optionsArg.serviceName,
|
name: optionsArg.serviceName,
|
||||||
@@ -155,7 +141,7 @@ export class CloudlyAppCatalogManager {
|
|||||||
imageVersion: this.getImageTag(config.image),
|
imageVersion: this.getImageTag(config.image),
|
||||||
deployOnPush: false,
|
deployOnPush: false,
|
||||||
appTemplateId: optionsArg.appId,
|
appTemplateId: optionsArg.appId,
|
||||||
appTemplateVersion: version,
|
appTemplateVersion: appStoreVersion,
|
||||||
environment: envVars,
|
environment: envVars,
|
||||||
secretBundleId: secretBundle.id,
|
secretBundleId: secretBundle.id,
|
||||||
additionalSecretBundleIds: [],
|
additionalSecretBundleIds: [],
|
||||||
@@ -181,11 +167,11 @@ export class CloudlyAppCatalogManager {
|
|||||||
return service;
|
return service;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createCatalogImage(serviceNameArg: string, imageRefArg: string, descriptionArg: string): Promise<Image> {
|
private async createAppStoreImage(serviceNameArg: string, imageRefArg: string, descriptionArg: string): Promise<Image> {
|
||||||
const image = new Image();
|
const image = new Image();
|
||||||
image.id = await Image.getNewId();
|
image.id = await Image.getNewId();
|
||||||
image.data = {
|
image.data = {
|
||||||
name: `${serviceNameArg}-catalog-image`,
|
name: `${serviceNameArg}-appstore-image`,
|
||||||
description: descriptionArg,
|
description: descriptionArg,
|
||||||
location: {
|
location: {
|
||||||
internal: false,
|
internal: false,
|
||||||
@@ -210,8 +196,8 @@ export class CloudlyAppCatalogManager {
|
|||||||
const secretBundle = new SecretBundle();
|
const secretBundle = new SecretBundle();
|
||||||
secretBundle.id = plugins.smartunique.shortId(8);
|
secretBundle.id = plugins.smartunique.shortId(8);
|
||||||
secretBundle.data = {
|
secretBundle.data = {
|
||||||
name: `${serviceNameArg} catalog secrets`,
|
name: `${serviceNameArg} appstore secrets`,
|
||||||
description: `Generated catalog secret bundle for ${serviceNameArg}`,
|
description: `Generated appstore secret bundle for ${serviceNameArg}`,
|
||||||
type: 'service',
|
type: 'service',
|
||||||
includedSecretGroupIds: [],
|
includedSecretGroupIds: [],
|
||||||
includedTags: [],
|
includedTags: [],
|
||||||
@@ -222,7 +208,7 @@ export class CloudlyAppCatalogManager {
|
|||||||
return secretBundle;
|
return secretBundle;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createPlatformBindings(serviceArg: Service, configArg: IAppVersionConfig) {
|
private async createPlatformBindings(serviceArg: Service, configArg: IAppStoreVersionConfig) {
|
||||||
const requirements = configArg.platformRequirements || {};
|
const requirements = configArg.platformRequirements || {};
|
||||||
if (requirements.mongodb) {
|
if (requirements.mongodb) {
|
||||||
await PlatformBinding.upsertBinding({
|
await PlatformBinding.upsertBinding({
|
||||||
@@ -244,7 +230,7 @@ export class CloudlyAppCatalogManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private normalizeVolumes(volumesArg: IAppVersionConfig['volumes'] = []) {
|
private normalizeVolumes(volumesArg: IAppStoreVersionConfig['volumes'] = []) {
|
||||||
return volumesArg.map((volumeArg) => {
|
return volumesArg.map((volumeArg) => {
|
||||||
if (typeof volumeArg === 'string') {
|
if (typeof volumeArg === 'string') {
|
||||||
return { mountPath: volumeArg };
|
return { mountPath: volumeArg };
|
||||||
@@ -253,7 +239,7 @@ export class CloudlyAppCatalogManager {
|
|||||||
}).filter((volumeArg) => Boolean(volumeArg.mountPath));
|
}).filter((volumeArg) => Boolean(volumeArg.mountPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCatalogEnvVars(configArg: IAppVersionConfig, overridesArg: Record<string, string>): Record<string, string> {
|
private getAppStoreEnvVars(configArg: IAppStoreVersionConfig, overridesArg: Record<string, string>): Record<string, string> {
|
||||||
const envVars: Record<string, string> = {};
|
const envVars: Record<string, string> = {};
|
||||||
const missingRequiredEnvVars: string[] = [];
|
const missingRequiredEnvVars: string[] = [];
|
||||||
for (const envVar of configArg.envVars || []) {
|
for (const envVar of configArg.envVars || []) {
|
||||||
@@ -274,12 +260,12 @@ export class CloudlyAppCatalogManager {
|
|||||||
return Object.values(envVarsArg).some((value) => value.includes(`\${${templateNameArg}}`));
|
return Object.values(envVarsArg).some((value) => value.includes(`\${${templateNameArg}}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
private assertSupportedPlatformRequirements(configArg: IAppVersionConfig) {
|
private assertSupportedPlatformRequirements(configArg: IAppStoreVersionConfig) {
|
||||||
const unsupported = Object.entries(configArg.platformRequirements || {})
|
const unsupported = Object.entries(configArg.platformRequirements || {})
|
||||||
.filter(([key, enabled]) => enabled && key !== 'mongodb' && key !== 's3')
|
.filter(([key, enabled]) => enabled && key !== 'mongodb' && key !== 's3')
|
||||||
.map(([key]) => key);
|
.map(([key]) => key);
|
||||||
if (unsupported.length > 0) {
|
if (unsupported.length > 0) {
|
||||||
throw new Error(`Cloudly catalog install does not yet support platform requirement(s): ${unsupported.join(', ')}`);
|
throw new Error(`Cloudly App Store install does not yet support platform requirement(s): ${unsupported.join(', ')}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,13 +280,4 @@ export class CloudlyAppCatalogManager {
|
|||||||
this.cloudlyRef.authManager.adminIdentityGuard,
|
this.cloudlyRef.authManager.adminIdentityGuard,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchJson(pathArg: string): Promise<unknown> {
|
|
||||||
const url = `${this.repoBaseUrl.replace(/\/+$/, '')}/${pathArg}`;
|
|
||||||
const response = await fetch(url);
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status} for ${url}`);
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+2
-1
@@ -86,5 +86,6 @@ export {
|
|||||||
|
|
||||||
// @servezone scope
|
// @servezone scope
|
||||||
import * as servezoneInterfaces from '@serve.zone/interfaces';
|
import * as servezoneInterfaces from '@serve.zone/interfaces';
|
||||||
|
import * as servezoneAppstore from '@serve.zone/appstore';
|
||||||
|
|
||||||
export { servezoneInterfaces };
|
export { servezoneAppstore, servezoneInterfaces };
|
||||||
|
|||||||
@@ -472,7 +472,7 @@ export class CloudlyViewServices extends DeesElement {
|
|||||||
|
|
||||||
private async loadUpgradeInfo(serviceArg: plugins.interfaces.data.IService) {
|
private async loadUpgradeInfo(serviceArg: plugins.interfaces.data.IService) {
|
||||||
try {
|
try {
|
||||||
const response = await this.fireTypedRequest('getUpgradeableServices', {}) as { services: any[] };
|
const response = await this.fireTypedRequest('getUpgradeableAppStoreServices', {}) as { services: any[] };
|
||||||
this.upgradeInfo = response.services?.find((upgradeArg) => upgradeArg.serviceName === serviceArg.data.name) || null;
|
this.upgradeInfo = response.services?.find((upgradeArg) => upgradeArg.serviceName === serviceArg.data.name) || null;
|
||||||
} catch {
|
} catch {
|
||||||
this.upgradeInfo = null;
|
this.upgradeInfo = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user