Refactor/patch db (#2415)

* the only way to begin is by beginning

* chore: Convert over 3444 migration

* fix imports

* wip

* feat: convert volume

* convert: system.rs

* wip(convert): Setup

* wip properties

* wip notifications

* wip

* wip migration

* wip init

* wip auth/control

* wip action

* wip control

* wiip 034

* wip 344

* wip some more versions converted

* feat: Reserialize the version of the db

* wip rest of the versions

* wip s9pk/manifest

* wip wifi

* chore: net/keys

* chore: net/dns

* wip net/dhcp

* wip manager manager-map

* gut dependency errors

* wip update/mod

* detect breakages locally for updates

* wip: manager/mod

* wip: manager/health

* wip: backup/target/mod

* fix: Typo addresses

* clean control.rs

* fix system package id

* switch to btreemap for now

* config wip

* wip manager/mod

* install wip

Co-authored-by: J H <Blu-J@users.noreply.github.com>

* chore: Update the last of the errors

* feat: Change the prelude de to borrow

* feat: Adding in some more things

* chore: add to the prelude

* chore: Small fixes

* chore: Fixing the small errors

* wip: Cleaning up check errors

* wip: Fix some of the issues

* chore: Fix setup

* chore:fix version

* chore: prelude, mod, http_reader

* wip backup_bulk

* chore: Last of the errors

* upadte package.json

* chore: changes needed for a build

* chore: Removing some of the linting errors in the manager

* chore: Some linting 101

* fix: Wrong order of who owns what

* chore: Remove the unstable

* chore: Remove the test in the todo

* @dr-bonez did a refactoring on the backup

* chore: Make sure that there can only be one override guard at a time

* resolve most todos

* wip: Add some more tracing to debug an error

* wip: Use a mv instead of rename

* wip: Revert some of the missing code segments found earlier

* chore: Make the build

* chore: Something about the lib looks like it iis broken

* wip: More instrument and dev working

* kill netdummy before creating it

* better db analysis tools

* fixes from testing

* fix: Make add start the service

* fix status after install

* make wormhole

* fix missing icon file

* fix data url for icons

* fix: Bad deser

* bugfixes

* fix: Backup

* fix: Some of the restor

* fix: Restoring works

* update frontend patch-db types

* hack it in (#2424)

* hack it in

* optimize

* slightly cleaner

* handle config pointers

* dependency config errs

* fix compat

* cache docker

* fix dependency expectation

* fix dependency auto-config

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
Co-authored-by: Matt Hill <mattnine@protonmail.com>
Co-authored-by: J H <Blu-J@users.noreply.github.com>
Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com>
This commit is contained in:
J H
2023-09-27 15:46:48 -06:00
committed by GitHub
parent c305deab52
commit 9a202cc124
132 changed files with 7641 additions and 20541 deletions

17478
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -73,7 +73,7 @@
"node-jose": "^2.2.0",
"patch-db-client": "file: ../../../patch-db/client",
"pbkdf2": "^3.1.2",
"rxjs": "^7.5.6",
"rxjs": "^7.8.1",
"swiper": "^8.2.4",
"ts-matches": "^5.2.1",
"tslib": "^2.3.0",
@@ -103,7 +103,7 @@
"raw-loader": "^4.0.2",
"ts-node": "^10.7.0",
"tslint": "^6.1.3",
"typescript": "^4.6.3",
"typescript": "4.8.4",
"webpack-bundle-analyzer": "^4.8.0"
},
"husky": {

View File

@@ -14,7 +14,7 @@
<div class="welcome-header">
<h1>Welcome to StartOS</h1>
</div>
<widget-list></widget-list>
<!-- <widget-list></widget-list> -->
</ng-container>
<ng-template #list>

View File

@@ -1,5 +1,5 @@
import { Pipe, PipeTransform } from '@angular/core'
import { Observable } from 'rxjs'
import { Observable, combineLatest } from 'rxjs'
import { filter, map, startWith } from 'rxjs/operators'
import {
DataModel,
@@ -7,16 +7,23 @@ import {
} from 'src/app/services/patch-db/data-model'
import { getPackageInfo, PkgInfo } from '../../../util/get-package-info'
import { PatchDB } from 'patch-db-client'
import { DepErrorService } from 'src/app/services/dep-error.service'
@Pipe({
name: 'packageInfo',
})
export class PackageInfoPipe implements PipeTransform {
constructor(private readonly patch: PatchDB<DataModel>) {}
constructor(
private readonly patch: PatchDB<DataModel>,
private readonly depErrorService: DepErrorService,
) {}
transform(pkg: PackageDataEntry): Observable<PkgInfo> {
return this.patch
.watch$('package-data', pkg.manifest.id)
.pipe(filter(Boolean), startWith(pkg), map(getPackageInfo))
return combineLatest([
this.patch
.watch$('package-data', pkg.manifest.id)
.pipe(filter(Boolean), startWith(pkg)),
this.depErrorService.depErrors$,
]).pipe(map(([pkg, depErrors]) => getPackageInfo(pkg, depErrors)))
}
}

View File

@@ -18,8 +18,6 @@ import { AppShowAdditionalComponent } from './components/app-show-additional/app
import { HealthColorPipe } from './pipes/health-color.pipe'
import { ToHealthChecksPipe } from './pipes/to-health-checks.pipe'
import { ToButtonsPipe } from './pipes/to-buttons.pipe'
import { ToDependenciesPipe } from './pipes/to-dependencies.pipe'
import { ToStatusPipe } from './pipes/to-status.pipe'
import { ProgressDataPipe } from './pipes/progress-data.pipe'
const routes: Routes = [
@@ -36,8 +34,6 @@ const routes: Routes = [
ProgressDataPipe,
ToHealthChecksPipe,
ToButtonsPipe,
ToDependenciesPipe,
ToStatusPipe,
AppShowHeaderComponent,
AppShowProgressComponent,
AppShowStatusComponent,

View File

@@ -1,9 +1,9 @@
<ng-container *ngIf="pkg$ | async as pkg">
<ng-container *ngIf="pkgPlus$ | async as pkgPlus">
<!-- header -->
<app-show-header [pkg]="pkg"></app-show-header>
<app-show-header [pkg]="pkgPlus.pkg"></app-show-header>
<!-- content -->
<ion-content class="ion-padding with-widgets">
<ion-content *ngIf="pkgPlus.pkg as pkg" class="ion-padding with-widgets">
<!-- ** installing, updating, restoring ** -->
<ng-container *ngIf="showProgress(pkg); else installed">
<app-show-progress
@@ -15,33 +15,27 @@
<!-- Installed -->
<ng-template #installed>
<ng-container *ngIf="pkg | toDependencies as dependencies">
<ion-item-group *ngIf="pkg | toStatus as status">
<!-- ** status ** -->
<app-show-status
<ion-item-group *ngIf="pkgPlus.status as status">
<!-- ** status ** -->
<app-show-status [pkg]="pkg" [status]="status"></app-show-status>
<!-- ** installed && !backing-up ** -->
<ng-container *ngIf="isInstalled(pkg) && !isBackingUp(status)">
<!-- ** health checks ** -->
<app-show-health-checks
*ngIf="isRunning(status)"
[pkg]="pkg"
[dependencies]="dependencies"
[status]="status"
></app-show-status>
<!-- ** installed && !backing-up ** -->
<ng-container *ngIf="isInstalled(pkg) && !isBackingUp(status)">
<!-- ** health checks ** -->
<app-show-health-checks
*ngIf="isRunning(status)"
[pkg]="pkg"
></app-show-health-checks>
<!-- ** dependencies ** -->
<app-show-dependencies
*ngIf="dependencies.length"
[dependencies]="dependencies"
></app-show-dependencies>
<!-- ** menu ** -->
<app-show-menu [buttons]="pkg | toButtons"></app-show-menu>
<!-- ** additional ** -->
<app-show-additional [pkg]="pkg"></app-show-additional>
</ng-container>
</ion-item-group>
</ng-container>
></app-show-health-checks>
<!-- ** dependencies ** -->
<app-show-dependencies
*ngIf="pkgPlus.dependencies.length"
[dependencies]="pkgPlus.dependencies"
></app-show-dependencies>
<!-- ** menu ** -->
<app-show-menu [buttons]="pkg | toButtons"></app-show-menu>
<!-- ** additional ** -->
<app-show-additional [pkg]="pkg"></app-show-additional>
</ng-container>
</ion-item-group>
</ng-template>
</ion-content>
</ng-container>

View File

@@ -3,16 +3,37 @@ import { NavController } from '@ionic/angular'
import { PatchDB } from 'patch-db-client'
import {
DataModel,
InstalledPackageDataEntry,
Manifest,
PackageDataEntry,
PackageState,
} from 'src/app/services/patch-db/data-model'
import {
PackageStatus,
PrimaryStatus,
renderPkgStatus,
} from 'src/app/services/pkg-status-rendering.service'
import { tap } from 'rxjs/operators'
import { ActivatedRoute } from '@angular/router'
import { map, tap } from 'rxjs/operators'
import { ActivatedRoute, NavigationExtras } from '@angular/router'
import { getPkgId } from '@start9labs/shared'
import { ModalService } from 'src/app/services/modal.service'
import { DependentInfo } from 'src/app/types/dependent-info'
import {
DepErrorService,
DependencyErrorType,
PackageDependencyErrors,
} from 'src/app/services/dep-error.service'
import { combineLatest } from 'rxjs'
export interface DependencyInfo {
id: string
title: string
icon: string
version: string
errorText: string
actionText: string
action: () => any
}
const STATES = [
PackageState.Installing,
@@ -28,10 +49,21 @@ const STATES = [
export class AppShowPage {
private readonly pkgId = getPkgId(this.route)
readonly pkg$ = this.patch.watch$('package-data', this.pkgId).pipe(
tap(pkg => {
readonly pkgPlus$ = combineLatest([
this.patch.watch$('package-data'),
this.depErrorService.depErrors$,
]).pipe(
tap(([pkgs, _]) => {
// if package disappears, navigate to list page
if (!pkg) this.navCtrl.navigateRoot('/services')
if (!pkgs[this.pkgId]) this.navCtrl.navigateRoot('/services')
}),
map(([pkgs, depErrors]) => {
const pkg = pkgs[this.pkgId]
return {
pkg,
dependencies: this.getDepInfo(pkg, depErrors),
status: renderPkgStatus(pkg, depErrors),
}
}),
)
@@ -39,6 +71,8 @@ export class AppShowPage {
private readonly route: ActivatedRoute,
private readonly navCtrl: NavController,
private readonly patch: PatchDB<DataModel>,
private readonly modalService: ModalService,
private readonly depErrorService: DepErrorService,
) {}
isInstalled({ state }: PackageDataEntry): boolean {
@@ -56,4 +90,136 @@ export class AppShowPage {
showProgress({ state }: PackageDataEntry): boolean {
return STATES.includes(state)
}
private getDepInfo(
pkg: PackageDataEntry,
depErrors: PackageDependencyErrors,
): DependencyInfo[] {
const pkgInstalled = pkg.installed
if (!pkgInstalled) return []
return Object.keys(pkgInstalled['current-dependencies'])
.filter(id => !!pkgInstalled.manifest.dependencies[id])
.map(id => this.getDepValues(pkgInstalled, id, depErrors))
}
private getDepValues(
pkgInstalled: InstalledPackageDataEntry,
depId: string,
depErrors: PackageDependencyErrors,
): DependencyInfo {
const { errorText, fixText, fixAction } = this.getDepErrors(
pkgInstalled,
depId,
depErrors,
)
const depInfo = pkgInstalled['dependency-info'][depId]
return {
id: depId,
version: pkgInstalled.manifest.dependencies[depId].version, // do we want this version range?
title: depInfo?.manifest?.title || depId,
icon: depInfo?.icon || '',
errorText: errorText
? `${errorText}. ${pkgInstalled.manifest.title} will not work as expected.`
: '',
actionText: fixText || 'View',
action:
fixAction || (() => this.navCtrl.navigateForward(`/services/${depId}`)),
}
}
private getDepErrors(
pkgInstalled: InstalledPackageDataEntry,
depId: string,
depErrors: PackageDependencyErrors,
) {
const pkgManifest = pkgInstalled.manifest
const depError = depErrors[pkgInstalled.manifest.id][depId]
let errorText: string | null = null
let fixText: string | null = null
let fixAction: (() => any) | null = null
if (depError) {
if (depError.type === DependencyErrorType.NotInstalled) {
errorText = 'Not installed'
fixText = 'Install'
fixAction = () => this.fixDep(pkgManifest, 'install', depId)
} else if (depError.type === DependencyErrorType.IncorrectVersion) {
errorText = 'Incorrect version'
fixText = 'Update'
fixAction = () => this.fixDep(pkgManifest, 'update', depId)
} else if (depError.type === DependencyErrorType.ConfigUnsatisfied) {
errorText = 'Config not satisfied'
fixText = 'Auto config'
fixAction = () => this.fixDep(pkgManifest, 'configure', depId)
} else if (depError.type === DependencyErrorType.NotRunning) {
errorText = 'Not running'
fixText = 'Start'
} else if (depError.type === DependencyErrorType.HealthChecksFailed) {
errorText = 'Health check failed'
} else if (depError.type === DependencyErrorType.Transitive) {
errorText = 'Dependency has a dependency issue'
}
}
return {
errorText,
fixText,
fixAction,
}
}
private async fixDep(
pkgManifest: Manifest,
action: 'install' | 'update' | 'configure',
id: string,
): Promise<void> {
switch (action) {
case 'install':
case 'update':
return this.installDep(pkgManifest, id)
case 'configure':
return this.configureDep(pkgManifest, id)
}
}
private async installDep(
pkgManifest: Manifest,
depId: string,
): Promise<void> {
const version = pkgManifest.dependencies[depId].version
const dependentInfo: DependentInfo = {
id: pkgManifest.id,
title: pkgManifest.title,
version,
}
const navigationExtras: NavigationExtras = {
state: { dependentInfo },
}
await this.navCtrl.navigateForward(
`/marketplace/${depId}`,
navigationExtras,
)
}
private async configureDep(
pkgManifest: Manifest,
dependencyId: string,
): Promise<void> {
const dependentInfo: DependentInfo = {
id: pkgManifest.id,
title: pkgManifest.title,
}
await this.modalService.presentModalConfig({
pkgId: dependencyId,
dependentInfo,
})
}
}

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { DependencyInfo } from '../../pipes/to-dependencies.pipe'
import { DependencyInfo } from '../../app-show.page'
@Component({
selector: 'app-show-dependencies',

View File

@@ -15,7 +15,6 @@ import { ErrorToastService } from '@start9labs/shared'
import { AlertController, LoadingController } from '@ionic/angular'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ModalService } from 'src/app/services/modal.service'
import { DependencyInfo } from '../../pipes/to-dependencies.pipe'
import { hasCurrentDeps } from 'src/app/util/has-deps'
import { ConnectionService } from 'src/app/services/connection.service'
@@ -32,9 +31,6 @@ export class AppShowStatusComponent {
@Input()
status!: PackageStatus
@Input()
dependencies: DependencyInfo[] = []
PR = PrimaryRendering
readonly connected$ = this.connectionService.connected$
@@ -80,7 +76,7 @@ export class AppShowStatusComponent {
}
async tryStart(): Promise<void> {
if (this.dependencies.some(d => !!d.errorText)) {
if (this.status.dependency === 'warning') {
const depErrMsg = `${this.pkg.manifest.title} has unmet dependencies. It will not work as expected.`
const proceed = await this.presentAlertStart(depErrMsg)

View File

@@ -1,149 +0,0 @@
import { Pipe, PipeTransform } from '@angular/core'
import { NavigationExtras } from '@angular/router'
import { NavController } from '@ionic/angular'
import {
DependencyError,
DependencyErrorType,
PackageDataEntry,
} from 'src/app/services/patch-db/data-model'
import { DependentInfo } from 'src/app/types/dependent-info'
import { ModalService } from 'src/app/services/modal.service'
export interface DependencyInfo {
id: string
title: string
icon: string
version: string
errorText: string
actionText: string
action: () => any
}
@Pipe({
name: 'toDependencies',
})
export class ToDependenciesPipe implements PipeTransform {
constructor(
private readonly navCtrl: NavController,
private readonly modalService: ModalService,
) {}
transform(pkg: PackageDataEntry): DependencyInfo[] {
if (!pkg.installed) return []
return Object.keys(pkg.installed?.['current-dependencies'])
.filter(id => !!pkg.manifest.dependencies[id])
.map(id =>
this.setDepValues(pkg, id, pkg.installed!.status['dependency-errors']),
)
}
private setDepValues(
pkg: PackageDataEntry,
id: string,
errors: { [id: string]: DependencyError | null },
): DependencyInfo {
let errorText = ''
let actionText = 'View'
let action: () => any = () =>
this.navCtrl.navigateForward(`/services/${id}`)
const error = errors[id]
if (error) {
// health checks failed
if (
[
DependencyErrorType.InterfaceHealthChecksFailed,
DependencyErrorType.HealthChecksFailed,
].includes(error.type)
) {
errorText = 'Health check failed'
// not installed
} else if (error.type === DependencyErrorType.NotInstalled) {
errorText = 'Not installed'
actionText = 'Install'
action = () => this.fixDep(pkg, 'install', id)
// incorrect version
} else if (error.type === DependencyErrorType.IncorrectVersion) {
errorText = 'Incorrect version'
actionText = 'Update'
action = () => this.fixDep(pkg, 'update', id)
// not running
} else if (error.type === DependencyErrorType.NotRunning) {
errorText = 'Not running'
actionText = 'Start'
// config unsatisfied
} else if (error.type === DependencyErrorType.ConfigUnsatisfied) {
errorText = 'Config not satisfied'
actionText = 'Auto config'
action = () => this.fixDep(pkg, 'configure', id)
} else if (error.type === DependencyErrorType.Transitive) {
errorText = 'Dependency has a dependency issue'
}
errorText = `${errorText}. ${pkg.manifest.title} will not work as expected.`
}
const depInfo = pkg.installed?.['dependency-info'][id]
return {
id,
version: pkg.manifest.dependencies[id].version,
title: depInfo?.manifest?.title || id,
icon: depInfo?.icon || '',
errorText,
actionText,
action,
}
}
async fixDep(
pkg: PackageDataEntry,
action: 'install' | 'update' | 'configure',
id: string,
): Promise<void> {
switch (action) {
case 'install':
case 'update':
return this.installDep(pkg, id)
case 'configure':
return this.configureDep(pkg, id)
}
}
private async installDep(
pkg: PackageDataEntry,
depId: string,
): Promise<void> {
const version = pkg.manifest.dependencies[depId].version
const dependentInfo: DependentInfo = {
id: pkg.manifest.id,
title: pkg.manifest.title,
version,
}
const navigationExtras: NavigationExtras = {
state: { dependentInfo },
}
await this.navCtrl.navigateForward(
`/marketplace/${depId}`,
navigationExtras,
)
}
private async configureDep(
pkg: PackageDataEntry,
dependencyId: string,
): Promise<void> {
const dependentInfo: DependentInfo = {
id: pkg.manifest.id,
title: pkg.manifest.title,
}
await this.modalService.presentModalConfig({
pkgId: dependencyId,
dependentInfo,
})
}
}

View File

@@ -1,15 +0,0 @@
import { Pipe, PipeTransform } from '@angular/core'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import {
PackageStatus,
renderPkgStatus,
} from 'src/app/services/pkg-status-rendering.service'
@Pipe({
name: 'toStatus',
})
export class ToStatusPipe implements PipeTransform {
transform(pkg: PackageDataEntry): PackageStatus {
return renderPkgStatus(pkg)
}
}

View File

@@ -23,11 +23,10 @@ import {
import { ClientStorageService } from 'src/app/services/client-storage.service'
import { MarketplaceService } from 'src/app/services/marketplace.service'
import { hasCurrentDeps } from 'src/app/util/has-deps'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { Breakages } from 'src/app/services/api/api.types'
import { PatchDB } from 'patch-db-client'
import { getAllPackages } from 'src/app/util/get-package-data'
import { firstValueFrom } from 'rxjs'
import { dryUpdate } from 'src/app/util/dry-update'
@Component({
selector: 'marketplace-show-controls',
@@ -57,7 +56,6 @@ export class MarketplaceShowControlsComponent {
private readonly loadingCtrl: LoadingController,
private readonly emver: Emver,
private readonly errToast: ErrorToastService,
private readonly embassyApi: ApiService,
private readonly patch: PatchDB<DataModel>,
) {}
@@ -142,30 +140,19 @@ export class MarketplaceShowControlsComponent {
}
private async dryInstall(url: string) {
const loader = await this.loadingCtrl.create({
message: 'Checking dependent services...',
})
await loader.present()
const breakages = dryUpdate(
this.pkg.manifest,
await getAllPackages(this.patch),
this.emver,
)
const { id, version } = this.pkg.manifest
try {
const breakages = await this.embassyApi.dryUpdatePackage({
id,
version: `${version}`,
})
if (isEmptyObject(breakages)) {
this.install(url, loader)
} else {
await loader.dismiss()
const proceed = await this.presentAlertBreakages(breakages)
if (proceed) {
this.install(url)
}
if (isEmptyObject(breakages)) {
this.install(url)
} else {
const proceed = await this.presentAlertBreakages(breakages)
if (proceed) {
this.install(url)
}
} catch (e: any) {
this.errToast.present(e)
}
}
@@ -194,14 +181,11 @@ export class MarketplaceShowControlsComponent {
await alert.present()
}
private async install(url: string, loader?: HTMLIonLoadingElement) {
const message = 'Beginning Install...'
if (loader) {
loader.message = message
} else {
loader = await this.loadingCtrl.create({ message })
await loader.present()
}
private async install(url: string) {
const loader = await this.loadingCtrl.create({
message: 'Beginning Install...',
})
await loader.present()
const { id, version } = this.pkg.manifest
@@ -214,14 +198,10 @@ export class MarketplaceShowControlsComponent {
}
}
private async presentAlertBreakages(breakages: Breakages): Promise<boolean> {
private async presentAlertBreakages(breakages: string[]): Promise<boolean> {
let message: string =
'As a result of this update, the following services will no longer work properly and may crash:<ul>'
const localPkgs = await getAllPackages(this.patch)
const bullets = Object.keys(breakages).map(id => {
const title = localPkgs[id].manifest.title
return `<li><b>${title}</b></li>`
})
const bullets = breakages.map(title => `<li><b>${title}</b></li>`)
message = `${message}${bullets.join('')}</ul>`
return new Promise(async resolve => {

View File

@@ -1,5 +1,4 @@
import { Component, Inject } from '@angular/core'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { PatchDB } from 'patch-db-client'
import {
DataModel,
@@ -16,14 +15,10 @@ import {
import { Emver, isEmptyObject } from '@start9labs/shared'
import { Pipe, PipeTransform } from '@angular/core'
import { combineLatest, Observable } from 'rxjs'
import {
AlertController,
LoadingController,
NavController,
} from '@ionic/angular'
import { AlertController, NavController } from '@ionic/angular'
import { hasCurrentDeps } from 'src/app/util/has-deps'
import { getAllPackages } from 'src/app/util/get-package-data'
import { Breakages } from 'src/app/services/api/api.types'
import { dryUpdate } from 'src/app/util/dry-update'
interface UpdatesData {
hosts: StoreIdentity[]
@@ -48,11 +43,10 @@ export class UpdatesPage {
constructor(
@Inject(AbstractMarketplaceService)
readonly marketplaceService: MarketplaceService,
private readonly api: ApiService,
private readonly patch: PatchDB<DataModel>,
private readonly navCtrl: NavController,
private readonly loadingCtrl: LoadingController,
private readonly alertCtrl: AlertController,
private readonly emver: Emver,
) {}
viewInMarketplace(event: Event, url: string, id: string) {
@@ -77,55 +71,40 @@ export class UpdatesPage {
this.marketplaceService.updateQueue[id] = true
if (hasCurrentDeps(local)) {
this.dryUpdate(manifest, url)
this.dryInstall(manifest, url)
} else {
this.update(id, version, url)
this.install(id, version, url)
}
}
private async dryUpdate(manifest: MarketplaceManifest, url: string) {
const loader = await this.loadingCtrl.create({
message: 'Checking dependent services...',
})
await loader.present()
private async dryInstall(manifest: MarketplaceManifest, url: string) {
const { id, version, title } = manifest
const { id, version } = manifest
const breakages = dryUpdate(
manifest,
await getAllPackages(this.patch),
this.emver,
)
try {
const breakages = await this.api.dryUpdatePackage({
id,
version: `${version}`,
})
await loader.dismiss()
if (isEmptyObject(breakages)) {
this.update(id, version, url)
if (isEmptyObject(breakages)) {
this.install(id, version, url)
} else {
const proceed = await this.presentAlertBreakages(title, breakages)
if (proceed) {
this.install(id, version, url)
} else {
const proceed = await this.presentAlertBreakages(
manifest.title,
breakages,
)
if (proceed) {
this.update(id, version, url)
} else {
delete this.marketplaceService.updateQueue[id]
}
delete this.marketplaceService.updateQueue[id]
}
} catch (e: any) {
delete this.marketplaceService.updateQueue[id]
this.marketplaceService.updateErrors[id] = e.message
}
}
private async presentAlertBreakages(
title: string,
breakages: Breakages,
breakages: string[],
): Promise<boolean> {
let message: string = `As a result of updating ${title}, the following services will no longer work properly and may crash:<ul>`
const localPkgs = await getAllPackages(this.patch)
const bullets = Object.keys(breakages).map(id => {
const title = localPkgs[id].manifest.title
return `<li><b>${title}</b></li>`
const bullets = breakages.map(depTitle => {
return `<li><b>${depTitle}</b></li>`
})
message = `${message}${bullets.join('')}</ul>`
@@ -156,7 +135,7 @@ export class UpdatesPage {
})
}
private async update(id: string, version: string, url: string) {
private async install(id: string, version: string, url: string) {
try {
await this.marketplaceService.installPackage(id, version, url)
delete this.marketplaceService.updateQueue[id]

View File

@@ -1,7 +1,10 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { PatchDB } from 'patch-db-client'
import { map } from 'rxjs/operators'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import {
DataModel,
PackageDataEntry,
} from 'src/app/services/patch-db/data-model'
import { PrimaryStatus } from 'src/app/services/pkg-status-rendering.service'
import { getPackageInfo, PkgInfo } from '../../../../util/get-package-info'
@@ -20,11 +23,13 @@ export class HealthComponent {
'Transitioning',
] as const
readonly data$ = inject(PatchDB)
readonly data$ = inject(PatchDB<DataModel>)
.watch$('package-data')
.pipe(
map(data => {
const pkgs = Object.values<PackageDataEntry>(data).map(getPackageInfo)
const pkgs = Object.values<PackageDataEntry>(data).map(
pkg => getPackageInfo(pkg, {}), // @TODO hack because not currently using widget
)
const result = this.labels.reduce<Record<string, number>>(
(acc, label) => ({
...acc,

View File

@@ -1,5 +1,4 @@
import {
DependencyErrorType,
DockerIoFormat,
Manifest,
PackageDataEntry,
@@ -1889,7 +1888,7 @@ export module Mock {
started: new Date().toISOString(),
health: {},
},
'dependency-errors': {},
'dependency-config-errors': {},
},
'interface-addresses': {
ui: {
@@ -1935,7 +1934,7 @@ export module Mock {
main: {
status: PackageMainStatus.Stopped,
},
'dependency-errors': {},
'dependency-config-errors': {},
},
manifest: MockManifestBitcoinProxy,
'interface-addresses': {
@@ -1984,10 +1983,8 @@ export module Mock {
main: {
status: PackageMainStatus.Stopped,
},
'dependency-errors': {
'btc-rpc-proxy': {
type: DependencyErrorType.NotInstalled,
},
'dependency-config-errors': {
'btc-rpc-proxy': 'Username not found',
},
},
manifest: MockManifestLnd,

View File

@@ -4,7 +4,7 @@ import { PackagePropertiesVersioned } from 'src/app/util/properties.util'
import { ConfigSpec } from 'src/app/pkg-config/config-types'
import {
DataModel,
DependencyError,
HealthCheckResult,
Manifest,
} from 'src/app/services/patch-db/data-model'
import { StartOSDiskInfo, LogsRes, ServerLogsReq } from '@start9labs/shared'
@@ -201,9 +201,6 @@ export module RR {
} // package.install
export type InstallPackageRes = null
export type DryUpdatePackageReq = { id: string; version: string } // package.update.dry
export type DryUpdatePackageRes = Breakages
export type GetPackageConfigReq = { id: string } // package.config.get
export type GetPackageConfigRes = { spec: ConfigSpec; config: object }
@@ -465,3 +462,53 @@ declare global {
parse<T>(text: Stringified<T>, reviver?: (key: any, value: any) => any): T
}
}
export type Encrypted = {
encrypted: string
}
export type DependencyError =
| DependencyErrorNotInstalled
| DependencyErrorNotRunning
| DependencyErrorIncorrectVersion
| DependencyErrorConfigUnsatisfied
| DependencyErrorHealthChecksFailed
| DependencyErrorTransitive
export enum DependencyErrorType {
NotInstalled = 'not-installed',
NotRunning = 'not-running',
IncorrectVersion = 'incorrect-version',
ConfigUnsatisfied = 'config-unsatisfied',
HealthChecksFailed = 'health-checks-failed',
InterfaceHealthChecksFailed = 'interface-health-checks-failed',
Transitive = 'transitive',
}
export interface DependencyErrorNotInstalled {
type: DependencyErrorType.NotInstalled
}
export interface DependencyErrorNotRunning {
type: DependencyErrorType.NotRunning
}
export interface DependencyErrorIncorrectVersion {
type: DependencyErrorType.IncorrectVersion
expected: string // version range
received: string // version
}
export interface DependencyErrorConfigUnsatisfied {
type: DependencyErrorType.ConfigUnsatisfied
error: string
}
export interface DependencyErrorHealthChecksFailed {
type: DependencyErrorType.HealthChecksFailed
check: HealthCheckResult
}
export interface DependencyErrorTransitive {
type: DependencyErrorType.Transitive
}

View File

@@ -192,10 +192,6 @@ export abstract class ApiService {
params: RR.InstallPackageReq,
): Promise<RR.InstallPackageRes>
abstract dryUpdatePackage(
params: RR.DryUpdatePackageReq,
): Promise<RR.DryUpdatePackageRes>
abstract getPackageConfig(
params: RR.GetPackageConfigReq,
): Promise<RR.GetPackageConfigRes>

View File

@@ -354,12 +354,6 @@ export class LiveApiService extends ApiService {
return this.rpcRequest({ method: 'package.install', params })
}
async dryUpdatePackage(
params: RR.DryUpdatePackageReq,
): Promise<RR.DryUpdatePackageRes> {
return this.rpcRequest({ method: 'package.update.dry', params })
}
async getPackageConfig(
params: RR.GetPackageConfigReq,
): Promise<RR.GetPackageConfigRes> {

View File

@@ -10,7 +10,6 @@ import {
} from 'patch-db-client'
import {
DataModel,
DependencyErrorType,
InstallProgress,
PackageDataEntry,
PackageMainStatus,
@@ -631,22 +630,6 @@ export class MockApiService extends ApiService {
return this.withRevision(patch)
}
async dryUpdatePackage(
params: RR.DryUpdatePackageReq,
): Promise<RR.DryUpdatePackageRes> {
await pauseFor(2000)
return {
lnd: {
dependency: 'bitcoind',
error: {
type: DependencyErrorType.IncorrectVersion,
expected: '>0.23.0',
received: params.version,
},
},
}
}
async getPackageConfig(
params: RR.GetPackageConfigReq,
): Promise<RR.GetPackageConfigRes> {

View File

@@ -1,6 +1,5 @@
import {
DataModel,
DependencyErrorType,
DockerIoFormat,
HealthResult,
Manifest,
@@ -438,7 +437,7 @@ export const mockPatchData: DataModel = {
},
},
},
'dependency-errors': {},
'dependency-config-errors': {},
},
'interface-addresses': {
ui: {
@@ -637,11 +636,8 @@ export const mockPatchData: DataModel = {
main: {
status: PackageMainStatus.Stopped,
},
'dependency-errors': {
'btc-rpc-proxy': {
type: DependencyErrorType.ConfigUnsatisfied,
error: 'This is a config unsatisfied error',
},
'dependency-config-errors': {
'btc-rpc-proxy': 'This is a config unsatisfied error',
},
},
'interface-addresses': {

View File

@@ -0,0 +1,211 @@
import { Injectable } from '@angular/core'
import { Emver } from '@start9labs/shared'
import { map, shareReplay } from 'rxjs/operators'
import { PatchDB } from 'patch-db-client'
import {
DataModel,
HealthCheckResult,
HealthResult,
InstalledPackageDataEntry,
PackageMainStatus,
} from './patch-db/data-model'
export type PackageDependencyErrors = Record<string, DependencyErrors>
export type DependencyErrors = Record<string, DependencyError | null>
@Injectable({
providedIn: 'root',
})
export class DepErrorService {
readonly depErrors$ = this.patch.watch$('package-data').pipe(
map(pkgs =>
Object.keys(pkgs)
.map(id => ({
id,
depth: dependencyDepth(pkgs, id),
}))
.sort((a, b) => (b.depth > a.depth ? -1 : 1))
.reduce(
(errors, { id }): PackageDependencyErrors => ({
...errors,
[id]: this.getDepErrors(pkgs, id, errors),
}),
{} as PackageDependencyErrors,
),
),
shareReplay(1),
)
constructor(
private readonly emver: Emver,
private readonly patch: PatchDB<DataModel>,
) {}
private getDepErrors(
pkgs: DataModel['package-data'],
pkgId: string,
outerErrors: PackageDependencyErrors,
): DependencyErrors {
const pkgInstalled = pkgs[pkgId].installed
if (!pkgInstalled) return {}
return currentDeps(pkgs, pkgId).reduce(
(innerErrors, depId): DependencyErrors => ({
...innerErrors,
[depId]: this.getDepError(pkgs, pkgInstalled, depId, outerErrors),
}),
{} as DependencyErrors,
)
}
private getDepError(
pkgs: DataModel['package-data'],
pkgInstalled: InstalledPackageDataEntry,
depId: string,
outerErrors: PackageDependencyErrors,
): DependencyError | null {
const depInstalled = pkgs[depId]?.installed
// not installed
if (!depInstalled) {
return {
type: DependencyErrorType.NotInstalled,
}
}
const pkgManifest = pkgInstalled.manifest
const depManifest = depInstalled.manifest
// incorrect version
if (
!this.emver.satisfies(
depManifest.version,
pkgManifest.dependencies[depId].version,
)
) {
return {
type: DependencyErrorType.IncorrectVersion,
expected: pkgManifest.dependencies[depId].version,
received: depManifest.version,
}
}
// invalid config
if (
Object.values(pkgInstalled.status['dependency-config-errors']).some(
err => !!err,
)
) {
return {
type: DependencyErrorType.ConfigUnsatisfied,
}
}
const depStatus = depInstalled.status.main.status
// not running
if (
depStatus !== PackageMainStatus.Running &&
depStatus !== PackageMainStatus.Starting &&
!(
depStatus === PackageMainStatus.BackingUp &&
depInstalled.status.main.started
)
) {
return {
type: DependencyErrorType.NotRunning,
}
}
// health check failure
if (depStatus === PackageMainStatus.Running) {
for (let id of pkgInstalled['current-dependencies'][depId][
'health-checks'
]) {
if (
depInstalled.status.main.health[id].result !== HealthResult.Success
) {
return {
type: DependencyErrorType.HealthChecksFailed,
check: depInstalled.status.main.health[id],
}
}
}
}
// transitive
const transitiveError = currentDeps(pkgs, depId).some(transitiveId =>
Object.values(outerErrors[transitiveId]).some(err => !!err),
)
if (transitiveError) {
return {
type: DependencyErrorType.Transitive,
}
}
return null
}
}
function currentDeps(pkgs: DataModel['package-data'], id: string): string[] {
return Object.keys(
pkgs[id]?.installed?.['current-dependencies'] || {},
).filter(depId => depId !== id)
}
function dependencyDepth(
pkgs: DataModel['package-data'],
id: string,
depth = 0,
): number {
return currentDeps(pkgs, id).reduce(
(prev, depId) => dependencyDepth(pkgs, depId, prev + 1),
depth,
)
}
export type DependencyError =
| DependencyErrorNotInstalled
| DependencyErrorNotRunning
| DependencyErrorIncorrectVersion
| DependencyErrorConfigUnsatisfied
| DependencyErrorHealthChecksFailed
| DependencyErrorTransitive
export enum DependencyErrorType {
NotInstalled = 'notInstalled',
NotRunning = 'notRunning',
IncorrectVersion = 'incorrectVersion',
ConfigUnsatisfied = 'configUnsatisfied',
HealthChecksFailed = 'healthChecksFailed',
Transitive = 'transitive',
}
export interface DependencyErrorNotInstalled {
type: DependencyErrorType.NotInstalled
}
export interface DependencyErrorNotRunning {
type: DependencyErrorType.NotRunning
}
export interface DependencyErrorIncorrectVersion {
type: DependencyErrorType.IncorrectVersion
expected: string // version range
received: string // version
}
export interface DependencyErrorConfigUnsatisfied {
type: DependencyErrorType.ConfigUnsatisfied
}
export interface DependencyErrorHealthChecksFailed {
type: DependencyErrorType.HealthChecksFailed
check: HealthCheckResult
}
export interface DependencyErrorTransitive {
type: DependencyErrorType.Transitive
}

View File

@@ -277,7 +277,7 @@ export interface Action {
export interface Status {
configured: boolean
main: MainStatus
'dependency-errors': { [id: string]: DependencyError | null }
'dependency-config-errors': { [id: string]: string | null }
}
export type MainStatus =
@@ -362,52 +362,6 @@ export interface HealthCheckResultFailure {
error: string
}
export type DependencyError =
| DependencyErrorNotInstalled
| DependencyErrorNotRunning
| DependencyErrorIncorrectVersion
| DependencyErrorConfigUnsatisfied
| DependencyErrorHealthChecksFailed
| DependencyErrorTransitive
export enum DependencyErrorType {
NotInstalled = 'not-installed',
NotRunning = 'not-running',
IncorrectVersion = 'incorrect-version',
ConfigUnsatisfied = 'config-unsatisfied',
HealthChecksFailed = 'health-checks-failed',
InterfaceHealthChecksFailed = 'interface-health-checks-failed',
Transitive = 'transitive',
}
export interface DependencyErrorNotInstalled {
type: DependencyErrorType.NotInstalled
}
export interface DependencyErrorNotRunning {
type: DependencyErrorType.NotRunning
}
export interface DependencyErrorIncorrectVersion {
type: DependencyErrorType.IncorrectVersion
expected: string // version range
received: string // version
}
export interface DependencyErrorConfigUnsatisfied {
type: DependencyErrorType.ConfigUnsatisfied
error: string
}
export interface DependencyErrorHealthChecksFailed {
type: DependencyErrorType.HealthChecksFailed
check: HealthCheckResult
}
export interface DependencyErrorTransitive {
type: DependencyErrorType.Transitive
}
export interface InstallProgress {
readonly size: number | null
readonly downloaded: number

View File

@@ -1,11 +1,13 @@
import { isEmptyObject } from '@start9labs/shared'
import {
InstalledPackageDataEntry,
MainStatusStarting,
PackageDataEntry,
PackageMainStatus,
PackageState,
Status,
} from 'src/app/services/patch-db/data-model'
import { PackageDependencyErrors } from './dep-error.service'
export interface PackageStatus {
primary: PrimaryStatus
@@ -13,16 +15,21 @@ export interface PackageStatus {
health: HealthStatus | null
}
export function renderPkgStatus(pkg: PackageDataEntry): PackageStatus {
export function renderPkgStatus(
pkg: PackageDataEntry,
depErrors: PackageDependencyErrors,
): PackageStatus {
let primary: PrimaryStatus
let dependency: DependencyStatus | null = null
let health: HealthStatus | null = null
const hasHealthChecks = !isEmptyObject(pkg.manifest['health-checks'])
if (pkg.state === PackageState.Installed && pkg.installed) {
primary = getPrimaryStatus(pkg.installed.status)
dependency = getDependencyStatus(pkg)
health = getHealthStatus(pkg.installed.status, hasHealthChecks)
dependency = getDependencyStatus(pkg.installed, depErrors)
health = getHealthStatus(
pkg.installed.status,
!isEmptyObject(pkg.manifest['health-checks']),
)
} else {
primary = pkg.state as string as PrimaryStatus
}
@@ -40,15 +47,13 @@ function getPrimaryStatus(status: Status): PrimaryStatus {
}
}
function getDependencyStatus(pkg: PackageDataEntry): DependencyStatus | null {
const installed = pkg.installed
if (!installed || isEmptyObject(installed['current-dependencies']))
return null
const depErrors = installed.status['dependency-errors']
const depIds = Object.keys(depErrors).filter(key => !!depErrors[key])
return depIds.length ? DependencyStatus.Warning : DependencyStatus.Satisfied
function getDependencyStatus(
pkg: InstalledPackageDataEntry,
depErrors: PackageDependencyErrors,
): DependencyStatus {
return Object.values(depErrors[pkg.manifest.id]).some(err => !!err)
? DependencyStatus.Warning
: DependencyStatus.Satisfied
}
function getHealthStatus(

View File

@@ -0,0 +1,17 @@
import { Emver } from '@start9labs/shared'
import { DataModel } from '../services/patch-db/data-model'
export function dryUpdate(
{ id, version }: { id: string; version: string },
pkgs: DataModel['package-data'],
emver: Emver,
): string[] {
return Object.values(pkgs)
.filter(
pkg =>
Object.keys(pkg.installed?.['current-dependencies'] || {}).some(
pkgId => pkgId === id,
) && !emver.satisfies(version, pkg.manifest.dependencies[id].version),
)
.map(pkg => pkg.manifest.title)
}

View File

@@ -14,6 +14,6 @@ export async function getPackage(
export async function getAllPackages(
patch: PatchDB<DataModel>,
): Promise<Record<string, PackageDataEntry>> {
): Promise<DataModel['package-data']> {
return firstValueFrom(patch.watch$('package-data'))
}

View File

@@ -10,9 +10,13 @@ import {
import { ProgressData } from 'src/app/types/progress-data'
import { Subscription } from 'rxjs'
import { packageLoadingProgress } from './package-loading-progress'
import { PackageDependencyErrors } from '../services/dep-error.service'
export function getPackageInfo(entry: PackageDataEntry): PkgInfo {
const statuses = renderPkgStatus(entry)
export function getPackageInfo(
entry: PackageDataEntry,
depErrors: PackageDependencyErrors,
): PkgInfo {
const statuses = renderPkgStatus(entry, depErrors)
const primaryRendering = PrimaryRendering[statuses.primary]
return {