Merge branch 'integration/new-container-runtime' of github.com:Start9Labs/start-os into rebase/feat/domains

This commit is contained in:
Matt Hill
2024-03-22 10:10:55 -06:00
52 changed files with 3977 additions and 4678 deletions

7001
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -73,7 +73,7 @@
"monaco-editor": "^0.33.0",
"mustache": "^4.2.0",
"ng-qrcode": "^17.0.0",
"node-jose": "^2.1.1",
"node-jose": "^2.2.0",
"patch-db-client": "file:../patch-db/client",
"pbkdf2": "^3.1.2",
"rxjs": "^7.5.6",

View File

@@ -2,7 +2,7 @@
<!-- color background -->
<div class="background">
<img
[src]="pkg | mimeType | trustUrl"
[src]="pkg.icon| trustUrl"
alt="{{ pkg.manifest.title }} Icon"
/>
</div>
@@ -10,7 +10,7 @@
<div class="overlay"></div>
<!-- icon -->
<img
[src]="pkg | mimeType | trustUrl"
[src]="pkg.icon | trustUrl"
class="icon box-shadow-lg"
alt="{{ pkg.manifest.title }} Icon"
/>

View File

@@ -3,11 +3,10 @@ import { NgModule } from '@angular/core'
import { RouterModule } from '@angular/router'
import { SharedPipesModule } from '@start9labs/shared'
import { ItemComponent } from './item.component'
import { MimeTypePipeModule } from '../../../pipes/mime-type.pipe'
@NgModule({
declarations: [ItemComponent],
exports: [ItemComponent],
imports: [CommonModule, RouterModule, SharedPipesModule, MimeTypePipeModule],
imports: [CommonModule, RouterModule, SharedPipesModule],
})
export class ItemModule {}

View File

@@ -1,22 +1,19 @@
<div class="outer-container">
<div class="inner-container">
<tui-avatar class="dep-img" [src]="getImage(dep.key)"></tui-avatar>
<tui-avatar class="dep-img" [src]="pkg['dependency-metadata'][dep.key].icon"></tui-avatar>
<div class="wrapper-margin">
<div class="inner-container-title">
<span>
{{ getTitle(dep.key) }}
{{ pkg['dependency-metadata'][dep.key].title || dep.key }}
</span>
<p>
<ng-container [ngSwitch]="dep.value.requirement.type">
<span *ngSwitchCase="'required'">(required)</span>
<span *ngSwitchCase="'opt-out'">(required by default)</span>
<span *ngSwitchCase="'opt-in'">(optional)</span>
</ng-container>
@if (dep.value.optional) {
<span>(optional)</span>
} @else {
<span>(required)</span>
}
</p>
</div>
<span class="inner-container-version">
{{ dep.value.version | displayEmver }}
</span>
<span class="inner-container-description">
{{ dep.value.description }}
</span>

View File

@@ -36,16 +36,6 @@
}
}
&-version {
font-size: 0.875rem;
line-height: 1.25rem;
color: rgb(250 250 250 / 0.7);
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
}
&-description {
font-size: 0.875rem;
line-height: 1.25rem;

View File

@@ -14,14 +14,4 @@ export class DependenciesComponent {
@Input({ required: true })
dep!: KeyValue<string, Dependency>
getImage(key: string): string {
const icon = this.pkg['dependency-metadata'][key]?.icon
// @TODO fix when registry api is updated to include mimetype in icon url
return icon ? `data:image/png;base64,${icon}` : key.substring(0, 2)
}
getTitle(key: string): string {
return this.pkg['dependency-metadata'][key]?.title || key
}
}

View File

@@ -1,34 +0,0 @@
import { NgModule, Pipe, PipeTransform } from '@angular/core'
import { MarketplacePkg } from '../types'
@Pipe({
name: 'mimeType',
})
export class MimeTypePipe implements PipeTransform {
transform(pkg: MarketplacePkg): string {
if (pkg.icon.startsWith('data:')) return pkg.icon
if (pkg.manifest.assets.icon) {
switch (pkg.manifest.assets.icon.split('.').pop()) {
case 'png':
return `data:image/png;base64,${pkg.icon}`
case 'jpeg':
case 'jpg':
return `data:image/jpeg;base64,${pkg.icon}`
case 'gif':
return `data:image/gif;base64,${pkg.icon}`
case 'svg':
return `data:image/svg+xml;base64,${pkg.icon}`
default:
return `data:image/png;base64,${pkg.icon}`
}
}
return `data:image/png;base64,${pkg.icon}`
}
}
@NgModule({
declarations: [MimeTypePipe],
exports: [MimeTypePipe],
})
export class MimeTypePipeModule {}

View File

@@ -20,7 +20,6 @@ export * from './pages/show/screenshots/screenshots.component'
export * from './pages/show/hero/hero.component'
export * from './pipes/filter-packages.pipe'
export * from './pipes/mime-type.pipe'
export * from './components/store-icon/store-icon.component'
export * from './components/store-icon/store-icon.component.module'

View File

@@ -38,6 +38,7 @@ export interface MarketplacePkg {
export interface DependencyMetadata {
title: string
icon: Url
optional: boolean
hidden: boolean
}
@@ -50,9 +51,6 @@ export interface Manifest {
short: string
long: string
}
assets: {
icon: Url // filename
}
replaces?: string[]
'release-notes': string
license: string // name of license
@@ -70,21 +68,10 @@ export interface Manifest {
}
dependencies: Record<string, Dependency>
'os-version': string
'has-config': boolean
}
export interface Dependency {
version: string
requirement:
| {
type: 'opt-in'
how: string
}
| {
type: 'opt-out'
how: string
}
| {
type: 'required'
}
description: string | null
optional: boolean
}

View File

@@ -30,7 +30,7 @@ export class ServiceInterfaceRoute {
readonly interfaceInfo$ = inject(PatchDB<DataModel>).watch$(
'package-data',
this.context.packageId,
'installed',
'state-info',
'interfaceInfo',
this.context.interfaceId,
)

View File

@@ -1,13 +1,16 @@
import { Pipe, PipeTransform } from '@angular/core'
import { InstallProgress } from 'src/app/services/patch-db/data-model'
import { packageLoadingProgress } from 'src/app/util/package-loading-progress'
import { Progress } from 'src/app/services/patch-db/data-model'
@Pipe({
name: 'installProgress',
standalone: true,
})
export class InstallProgressPipe implements PipeTransform {
transform(installProgress?: InstallProgress): number {
return packageLoadingProgress(installProgress)?.totalProgress || 0
export class InstallingProgressDisplayPipe implements PipeTransform {
transform(progress: Progress): string {
if (progress === true) return 'finalizing'
if (progress === false || !progress.total) return 'unknown %'
const percentage = Math.round((100 * progress.done) / progress.total)
return percentage < 99 ? String(percentage) + '%' : 'finalizing'
}
}

View File

@@ -18,12 +18,12 @@ import {
MarketplacePkg,
} from '@start9labs/marketplace'
import { Log } from '@start9labs/shared'
import { unionSelectKey } from '@start9labs/start-sdk/lib/config/configTypes'
import { List } from '@start9labs/start-sdk/lib/config/builder/list'
import { Value } from '@start9labs/start-sdk/lib/config/builder/value'
import { Variants } from '@start9labs/start-sdk/lib/config/builder/variants'
import { Config } from '@start9labs/start-sdk/lib/config/builder/config'
import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec'
import { Config } from '@start9labs/start-sdk/cjs/sdk/lib/config/builder/config'
import { Value } from '@start9labs/start-sdk/cjs/sdk/lib/config/builder/value'
import { Variants } from '@start9labs/start-sdk/cjs/sdk/lib/config/builder/variants'
import { List } from '@start9labs/start-sdk/cjs/sdk/lib/config/builder/list'
import { unionSelectKey } from '@start9labs/start-sdk/cjs/sdk/lib/config/configTypes'
export module Mock {
export const ServerUpdated: ServerStatusInfo = {
@@ -67,9 +67,6 @@ export module Mock {
short: 'A Bitcoin full node by Bitcoin Core.',
long: 'Bitcoin is a decentralized consensus protocol and settlement network.',
},
assets: {
icon: 'icon.png',
},
replaces: ['banks', 'governments'],
'release-notes': 'Taproot, Schnorr, and more.',
license: 'MIT',
@@ -86,8 +83,9 @@ export module Mock {
start: 'Starting Bitcoin is good for your health.',
stop: null,
},
'os-version': '0.2.12',
dependencies: {},
'os-version': '0.4.0',
'has-config': true,
}
export const MockManifestLnd: Manifest = {
@@ -98,11 +96,7 @@ export module Mock {
short: 'A bolt spec compliant client.',
long: 'More info about LND. More info about LND. More info about LND.',
},
assets: {
icon: 'icon.png',
},
'release-notes':
'* Dual funded channels! And lots more amazing new features. Also includes several bugfixes and performance enhancements.',
'release-notes': 'Dual funded channels!',
license: 'MIT',
'wrapper-repo': 'https://github.com/start9labs/lnd-wrapper',
'upstream-repo': 'https://github.com/lightningnetwork/lnd',
@@ -117,26 +111,19 @@ export module Mock {
start: 'Starting LND is good for your health.',
stop: null,
},
'os-version': '0.2.12',
dependencies: {
bitcoind: {
version: '=0.21.0',
description: 'LND needs bitcoin to live.',
requirement: {
type: 'opt-out',
how: 'You can use an external node from your server if you prefer.',
},
optional: true,
},
'btc-rpc-proxy': {
version: '>=0.2.2',
description:
'As long as Bitcoin is pruned, LND needs Bitcoin Proxy to fetch block over the P2P network.',
requirement: {
type: 'opt-in',
how: `To use Proxy's user management system, go to LND config and select Bitcoin Proxy under Bitcoin config.`,
},
optional: true,
},
},
'os-version': '0.4.0',
'has-config': true,
}
export const MockManifestBitcoinProxy: Manifest = {
@@ -148,9 +135,6 @@ export module Mock {
short: 'A super charger for your Bitcoin node.',
long: 'More info about Bitcoin Proxy. More info about Bitcoin Proxy. More info about Bitcoin Proxy.',
},
assets: {
icon: 'icon.png',
},
'release-notes': 'Even better support for Bitcoin and wallets!',
license: 'MIT',
'wrapper-repo': 'https://github.com/start9labs/btc-rpc-proxy-wrapper',
@@ -165,27 +149,27 @@ export module Mock {
start: null,
stop: null,
},
'os-version': '0.2.12',
dependencies: {
bitcoind: {
version: '>=0.20.0',
description: 'Bitcoin Proxy requires a Bitcoin node.',
requirement: {
type: 'required',
},
optional: false,
},
},
'os-version': '0.4.0',
'has-config': false,
}
export const BitcoinDep: DependencyMetadata = {
title: 'Bitcoin Core',
icon: BTC_ICON,
optional: false,
hidden: true,
}
export const ProxyDep: DependencyMetadata = {
title: 'Bitcoin Proxy',
icon: PROXY_ICON,
optional: true,
hidden: false,
}
@@ -1292,6 +1276,7 @@ export module Mock {
},
'dependency-config-errors': {},
},
actions: {}, // @TODO need mocks
'service-interfaces': {
ui: {
id: 'ui',
@@ -1508,11 +1493,6 @@ export module Mock {
},
},
},
'current-dependents': {
lnd: {
'health-checks': [],
},
},
'current-dependencies': {},
'dependency-info': {},
'marketplace-url': 'https://registry.start9.com/',
@@ -1535,6 +1515,7 @@ export module Mock {
},
'dependency-config-errors': {},
},
actions: {},
'service-interfaces': {
ui: {
id: 'ui',
@@ -1643,13 +1624,9 @@ export module Mock {
},
},
},
'current-dependents': {
lnd: {
'health-checks': [],
},
},
'current-dependencies': {
bitcoind: {
versionRange: '>=26.0.0',
'health-checks': [],
},
},
@@ -1681,6 +1658,7 @@ export module Mock {
'btc-rpc-proxy': 'Username not found',
},
},
actions: {},
'service-interfaces': {
grpc: {
id: 'grpc',
@@ -1893,12 +1871,13 @@ export module Mock {
},
},
},
'current-dependents': {},
'current-dependencies': {
bitcoind: {
versionRange: '>=26.0.0',
'health-checks': [],
},
'btc-rpc-proxy': {
versionRange: '>2.0.0', // @TODO
'health-checks': [],
},
},

View File

@@ -1,6 +1,5 @@
import { Dump, Revision } from 'patch-db-client'
import { MarketplacePkg, StoreInfo, Manifest } from '@start9labs/marketplace'
import { InputSpec } from '@start9labs/start-sdk/lib/config/configTypes'
import {
DataModel,
DomainInfo,
@@ -16,7 +15,8 @@ import {
FollowLogsRes,
FollowLogsReq,
} from '@start9labs/shared'
import { customSmtp } from '@start9labs/start-sdk/lib/config/configConstants'
import { customSmtp } from '@start9labs/start-sdk/cjs/sdk/lib/config/configConstants'
import { InputSpec } from '@start9labs/start-sdk/cjs/sdk/lib/config/configTypes'
export module RR {
// DB
@@ -596,8 +596,8 @@ export enum NotificationLevel {
export type NotificationData<T> = T extends 0
? null
: T extends 1
? BackupReport
: any
? BackupReport
: any
export interface BackupReport {
server: {

View File

@@ -866,7 +866,7 @@ export class MockApiService extends ApiService {
setTimeout(async () => {
for (let i = 0; i < ids.length; i++) {
const id = ids[i]
const appPath = `/package-data/${id}/installed/status/main/status`
const appPath = `/package-data/${id}/status/main/status`
const appPatch = [
{
op: PatchOp.REPLACE,
@@ -1032,7 +1032,7 @@ export class MockApiService extends ApiService {
const patch = [
{
op: PatchOp.REPLACE,
path: `/package-data/${params.id}/installed/status/configured`,
path: `/package-data/${params.id}/status/configured`,
value: true,
},
]
@@ -1079,7 +1079,7 @@ export class MockApiService extends ApiService {
}
async startPackage(params: RR.StartPackageReq): Promise<RR.StartPackageRes> {
const path = `/package-data/${params.id}/installed/status/main`
const path = `/package-data/${params.id}/status/main`
await pauseFor(2000)
@@ -1128,7 +1128,7 @@ export class MockApiService extends ApiService {
): Promise<RR.RestartPackageRes> {
// first enact stop
await pauseFor(2000)
const path = `/package-data/${params.id}/installed/status/main`
const path = `/package-data/${params.id}/status/main`
setTimeout(async () => {
const patch2: Operation<any>[] = [

View File

@@ -1,4 +1,9 @@
import { DataModel } from 'src/app/services/patch-db/data-model'
import {
DataModel,
HealthResult,
PackageMainStatus,
PackageState,
} from 'src/app/services/patch-db/data-model'
import { Mock } from './api.fixures'
export const mockPatchData: DataModel = {
@@ -31,18 +36,49 @@ export const mockPatchData: DataModel = {
id: 'abcdefgh',
version: '0.3.5.1',
country: 'us',
ui: {
lanHostname: 'adjective-noun.local',
torHostname: 'myveryownspecialtoraddress.onion',
ipInfo: {
eth0: {
wireless: false,
ipv4: '10.0.0.1',
ipv6: 'FE80:CD00:0000:0CDE:1257:0000:211E:729CD',
ui: [
{
kind: 'ip',
networkInterfaceId: 'elan0',
public: false,
hostname: {
kind: 'local',
value: 'adjective-noun.local',
port: null,
sslPort: 1111,
},
},
domainInfo: null,
},
{
kind: 'onion',
hostname: {
value: 'myveryownspecialtoraddress.onion',
port: 80,
sslPort: 443,
},
},
{
kind: 'ip',
networkInterfaceId: 'elan0',
public: false,
hostname: {
kind: 'ipv4',
value: '192.168.1.5',
port: null,
sslPort: 1111,
},
},
{
kind: 'ip',
networkInterfaceId: 'elan0',
public: false,
hostname: {
kind: 'ipv6',
value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]',
port: null,
sslPort: 1111,
},
},
],
network: {
domains: [],
start9ToSubdomain: null,
@@ -109,11 +145,49 @@ export const mockPatchData: DataModel = {
},
'package-data': {
bitcoind: {
...Mock.bitcoind,
manifest: {
...Mock.bitcoind.manifest,
version: '0.19.0',
'state-info': {
state: PackageState.Installed,
manifest: {
...Mock.MockManifestBitcoind,
version: '0.20.0',
},
},
icon: '/assets/img/service-icons/bitcoind.svg',
'last-backup': null,
status: {
configured: true,
main: {
status: PackageMainStatus.Running,
started: '2021-06-14T20:49:17.774Z',
health: {
'ephemeral-health-check': {
name: 'Ephemeral Health Check',
result: HealthResult.Starting,
},
'chain-state': {
name: 'Chain State',
result: HealthResult.Loading,
message: 'Bitcoin is syncing from genesis',
},
'p2p-interface': {
name: 'P2P',
result: HealthResult.Success,
message: 'Health check successful',
},
'rpc-interface': {
name: 'RPC',
result: HealthResult.Failure,
message: 'RPC interface unreachable.',
},
'unnecessary-health-check': {
name: 'Unnecessary Health Check',
result: HealthResult.Disabled,
},
},
},
'dependency-config-errors': {},
},
actions: {}, // @TODO
'service-interfaces': {
ui: {
id: 'ui',
@@ -330,22 +404,20 @@ export const mockPatchData: DataModel = {
},
},
},
'current-dependents': {
lnd: {
pointers: [],
'health-checks': [],
},
},
'current-dependencies': {},
'dependency-info': {},
'marketplace-url': 'https://registry.start9.com/',
'developer-key': 'developer-key',
'has-config': true,
outboundProxy: null,
},
lnd: {
...Mock.lnd,
manifest: {
...Mock.lnd.manifest,
version: '0.11.0',
'state-info': {
state: PackageState.Installed,
manifest: {
...Mock.MockManifestLnd,
version: '0.11.0',
},
},
icon: '/assets/img/service-icons/lnd.png',
'last-backup': null,
@@ -358,6 +430,7 @@ export const mockPatchData: DataModel = {
'btc-rpc-proxy': 'This is a config unsatisfied error',
},
},
actions: {},
'service-interfaces': {
grpc: {
id: 'grpc',
@@ -568,12 +641,13 @@ export const mockPatchData: DataModel = {
},
},
},
'current-dependents': {},
'current-dependencies': {
bitcoind: {
versionRange: '>=26.0.0',
'health-checks': [],
},
'btc-rpc-proxy': {
versionRange: '>2.0.0',
'health-checks': [],
},
},
@@ -589,6 +663,8 @@ export const mockPatchData: DataModel = {
},
'marketplace-url': 'https://registry.start9.com/',
'developer-key': 'developer-key',
'has-config': true,
outboundProxy: null,
},
},
}

View File

@@ -88,19 +88,14 @@ export class DepErrorService {
}
}
const pkgManifest = pkg['state-info'].manifest
const versionRange = pkg['current-dependencies'][depId].versionRange
const depManifest = dep['state-info'].manifest
// incorrect version
if (
!this.emver.satisfies(
depManifest.version,
pkgManifest.dependencies[depId].version,
)
) {
if (!this.emver.satisfies(depManifest.version, versionRange)) {
return {
type: DependencyErrorType.IncorrectVersion,
expected: pkgManifest.dependencies[depId].version,
expected: versionRange,
received: depManifest.version,
}
}

View File

@@ -1,9 +1,13 @@
import { InputSpec } from '@start9labs/start-sdk/lib/config/configTypes'
import { BackupJob, ServerNotifications } from '../api/api.types'
import { Url } from '@start9labs/shared'
import { Manifest } from '@start9labs/marketplace'
import { BackupJob, ServerNotifications } from '../api/api.types'
import { customSmtp } from '@start9labs/start-sdk/lib/config/configConstants'
import { types } from '@start9labs/start-sdk'
import { InputSpec } from '@start9labs/start-sdk/cjs/sdk/lib/config/configTypes'
import {
ActionMetadata,
HostnameInfo,
} from '@start9labs/start-sdk/cjs/sdk/lib/types'
import { customSmtp } from '@start9labs/start-sdk/cjs/sdk/lib/config/configConstants'
type ServiceInterfaceWithHostInfo = types.ServiceInterfaceWithHostInfo
export interface DataModel {
@@ -174,8 +178,8 @@ export type PackageDataEntry<T extends StateInfo = StateInfo> = {
'state-info': T
icon: Url
status: Status
actions: Record<string, ActionMetadata>
'last-backup': string | null
'current-dependents': { [id: string]: CurrentDependencyInfo }
'current-dependencies': { [id: string]: CurrentDependencyInfo }
'dependency-info': {
[id: string]: {
@@ -217,18 +221,10 @@ export enum PackageState {
}
export interface CurrentDependencyInfo {
versionRange: string
'health-checks': string[] // array of health check IDs
}
export interface Action {
name: string
description: string
warning: string | null
disabled: string | null
'input-spec': InputSpec | null
group: string | null
}
export interface Status {
configured: boolean
main: MainStatus
@@ -260,7 +256,7 @@ export interface MainStatusStarting {
export interface MainStatusRunning {
status: PackageMainStatus.Running
started: string // UTC date string
health: { [id: string]: HealthCheckResult }
health: Record<string, HealthCheckResult>
}
export interface MainStatusBackingUp {
@@ -307,7 +303,6 @@ export interface HealthCheckResultStarting {
export interface HealthCheckResultDisabled {
result: HealthResult.Disabled
reason: string
}
export interface HealthCheckResultSuccess {
@@ -322,7 +317,7 @@ export interface HealthCheckResultLoading {
export interface HealthCheckResultFailure {
result: HealthResult.Failure
error: string
message: string
}
export type InstallingInfo = {

View File

@@ -23,10 +23,7 @@ export function renderPkgStatus(
if (pkg['state-info'].state === PackageState.Installed) {
primary = getPrimaryStatus(pkg.status)
dependency = getDependencyStatus(depErrors)
health = getHealthStatus(
pkg.status,
!isEmptyObject(pkg['state-info'].manifest['health-checks']),
)
health = getHealthStatus(pkg.status)
} else {
primary = pkg['state-info'].state as string as PrimaryStatus
}
@@ -49,12 +46,16 @@ function getDependencyStatus(depErrors: PkgDependencyErrors): DependencyStatus {
}
function getHealthStatus(status: Status): HealthStatus | null {
if (status.main.status !== PackageMainStatus.Running) {
if (status.main.status !== PackageMainStatus.Running || !status.main.health) {
return null
}
const values = Object.values(status.main.health)
if (values.some(h => !h.result)) {
return HealthStatus.Waiting
}
if (values.some(h => h.result === 'failure')) {
return HealthStatus.Failure
}
@@ -63,7 +64,7 @@ function getHealthStatus(status: Status): HealthStatus | null {
return HealthStatus.Loading
}
if (values.some(h => !h.result || h.result === 'starting')) {
if (values.some(h => h.result === 'starting')) {
return HealthStatus.Starting
}

View File

@@ -13,7 +13,7 @@ export function dryUpdate(
Object.keys(pkg['current-dependencies'] || {}).some(
pkgId => pkgId === id,
) &&
!emver.satisfies(version, getManifest(pkg).dependencies[id].version),
!emver.satisfies(version, pkg['current-dependencies'][id].versionRange),
)
.map(pkg => getManifest(pkg).title)
}

View File

@@ -3,12 +3,12 @@ import {
DataModel,
InstalledState,
InstallingState,
Manifest,
PackageDataEntry,
PackageState,
UpdatingState,
} from 'src/app/services/patch-db/data-model'
import { firstValueFrom } from 'rxjs'
import { Manifest } from '@start9labs/marketplace'
export async function getPackage(
patch: PatchDB<DataModel>,

View File

@@ -1,8 +1,8 @@
import { PackageDataEntry } from '../services/patch-db/data-model'
import { getManifest } from './get-package-data'
export function hasCurrentDeps(pkg: PackageDataEntry): boolean {
return !!Object.keys(pkg['current-dependents']).filter(
depId => depId !== getManifest(pkg).id,
).length
export function hasCurrentDeps(
id: string,
pkgs: Record<string, PackageDataEntry>,
): boolean {
return !!Object.values(pkgs).some(pkg => !!pkg['current-dependencies'][id])
}