diff --git a/eshtek/apps-schema.ts b/eshtek/apps-schema.ts deleted file mode 100644 index 0beecd0..0000000 --- a/eshtek/apps-schema.ts +++ /dev/null @@ -1,201 +0,0 @@ -// Generated by ts-to-zod -import { z } from "zod"; -import { - AppJobAction, - AppSpec, - AppPermission, - AppsError, - AppsWarning, - AppsActions, -} from "./apps"; - -export const appJobActionSchema = z.nativeEnum(AppJobAction); - -export const appBasicsSchema = z.object({ - id: z.string(), - name: z.string(), - description: z.string(), - version: z.string(), - icon: z.string(), - train: z.string(), -}); - -export const appSpecSchema = z.nativeEnum(AppSpec); - -export const appPermissionSchema = z.nativeEnum(AppPermission); - -export const appsErrorSchema = z.nativeEnum(AppsError); - -export const appsWarningSchema = z.nativeEnum(AppsWarning); - -export const appsActionsSchema = z.nativeEnum(AppsActions); - -export const appsHealthSchema = z.object({ - healthy: z.boolean(), - errors: z.array(appsErrorSchema), - warnings: z.array(appsWarningSchema), - actions_available: z.array(appsActionsSchema), -}); - -const locationPreferenceIdSchema = z.any(); - -export const appMaintainerSchema = z.object({ - email: z.string(), - name: z.string(), - url: z.string(), -}); - -const appMetadataSchema = z.any(); - -const apiTimestampSchema = z.any(); - -export const availableAppSchema = z.object({ - healthy: z.boolean(), - installed: z.boolean(), - categories: z.array(z.string()), - name: z.string(), - title: z.string(), - description: z.string(), - app_readme: z.string(), - app_metadata: appMetadataSchema, - location: z.string(), - healthy_error: z.string(), - latest_version: z.string(), - latest_app_version: z.string(), - icon_url: z.string(), - train: z.string(), - catalog: z.string(), - last_update: apiTimestampSchema, - recommended: z.boolean(), - maintainers: z.array(appMaintainerSchema), - tags: z.array(z.string()), - home: z.string(), - latest_human_version: z.string(), - screenshots: z.array(z.string()), - sources: z.array(z.string()), - versions: z.unknown(), -}); - -const appStateSchema = z.any(); - -const fileAccessSchema = z.any(); - -const chartFormValueSchema = z.any(); - -export const appRequirementsSchema = z.object({ - permissions: z.array(appPermissionSchema), - specifications: z.array(appSpecSchema), - locations: z.array(locationPreferenceIdSchema), - ports: z.array(z.number()), -}); - -export const appRequirementsCheckSchema = z.object({ - permissions: z.object({ - met: z.array(appPermissionSchema), - unmet: z.array(appPermissionSchema), - }), - specifications: z.object({ - met: z.array(appSpecSchema), - unmet: z.array(appSpecSchema), - }), - locations: z.object({ - met: z.array(locationPreferenceIdSchema), - unmet: z.array(locationPreferenceIdSchema), - }), -}); - -export const appListingSchema = availableAppSchema.extend({ - hexos: z.boolean(), - recommended_during_setup: z.boolean().optional(), - requirements: appRequirementsSchema, -}); - -export const appInfoSchema = appBasicsSchema.extend({ - status: appStateSchema, - url_webui: z.string(), -}); - -export const appInfoDetailedSchema = appInfoSchema.extend({ - data: z.array(z.array(z.number())), -}); - -export const installationQuestionTypeSchema = z.enum(['text', 'number', 'select', 'boolean']); - -export const installationQuestionOptionSchema = z.object({ - text: z.string(), - value: z.union([z.string(), z.number(), z.boolean()]), -}); - -export const installationQuestionSchema = z.object({ - question: z.string(), - type: installationQuestionTypeSchema, - key: z.string(), - options: z.array(installationQuestionOptionSchema).optional(), - required: z.boolean().optional(), - default: z.union([z.string(), z.number(), z.boolean()]).optional(), -}); - -const appsInstallScriptV1Schema = z.object({ - version: z.literal(1), - ensure_directories_exists: z - .array( - z.union([ - z.string(), - z.object({ - path: z.string(), - network_share: z.boolean().optional(), - posix: z.boolean().optional(), - }), - ]), - ) - .optional(), - ensure_permissions_exists: z - .array( - z.object({ - path: z.string(), - username: z.string(), - access: fileAccessSchema, - posix: z - .object({ - groupname: z.string(), - }) - .optional(), - }), - ) - .optional(), - app_values: z.record(chartFormValueSchema), -}); - -const appsInstallScriptV2Schema = z.object({ - version: z.literal(2), - installation_questions: z.array(installationQuestionSchema).optional(), - ensure_directories_exists: z - .array( - z.union([ - z.string(), - z.object({ - path: z.string(), - network_share: z.boolean().optional(), - posix: z.boolean().optional(), - }), - ]), - ) - .optional(), - ensure_permissions_exists: z - .array( - z.object({ - path: z.string(), - username: z.string(), - access: fileAccessSchema, - posix: z - .object({ - groupname: z.string(), - }) - .optional(), - }), - ) - .optional(), - app_values: z.record(chartFormValueSchema), -}); - -export const appsInstallScriptSchema = z.union([appsInstallScriptV1Schema, appsInstallScriptV2Schema]); diff --git a/eshtek/apps.ts b/eshtek/apps.ts index fac1756..87ab391 100644 --- a/eshtek/apps.ts +++ b/eshtek/apps.ts @@ -89,7 +89,9 @@ export interface AppInfo extends AppBasics { status: AppState; url_webui: string; upgradeAvailable: boolean; + updatedScriptAvailable: boolean; latestVersion: string; + recommended: boolean; } export interface AppInfoDetailed extends AppInfo { data: number[][]; @@ -165,7 +167,22 @@ interface AppsInstallScriptV2 { app_values: Record; } -export type AppsInstallScript = AppsInstallScriptV1 | AppsInstallScriptV2; +interface AppsInstallScriptV3 extends Omit { + version: 3; + script: { + version: string; + updateCompatibility?: string; + changeLog?: string; + }; + requirements: AppRequirements; // Required in V3 +} + +export type AppsInstallScript = AppsInstallScriptV1 | AppsInstallScriptV2 | AppsInstallScriptV3; + +export interface AppConfiguration { + installScript?: AppsInstallScript | null; + questionResponses?: Record; +} export interface InstallScriptCuration { name: string; diff --git a/eshtek/errors.ts b/eshtek/errors.ts index d49b39d..afebb43 100644 --- a/eshtek/errors.ts +++ b/eshtek/errors.ts @@ -29,12 +29,22 @@ export enum GlobalErrorCode { PROCESS_INSTALL_SCRIPT_INVALID_MEMORY_VALUE = 'PROCESS_INSTALL_SCRIPT_INVALID_MEMORY_VALUE', PROCESS_INSTALL_SCRIPT_MISSING_QUESTION_RESPONSE = 'PROCESS_INSTALL_SCRIPT_MISSING_QUESTION_RESPONSE', PROCESS_INSTALL_SCRIPT_INVALID_QUESTION_RESPONSE_TYPE = 'PROCESS_INSTALL_SCRIPT_INVALID_QUESTION_RESPONSE_TYPE', + PROCESS_INSTALL_SCRIPT_MISSING_SCRIPT_METADATA = 'PROCESS_INSTALL_SCRIPT_MISSING_SCRIPT_METADATA', + PROCESS_INSTALL_SCRIPT_MISSING_REQUIREMENTS = 'PROCESS_INSTALL_SCRIPT_MISSING_REQUIREMENTS', + PROCESS_INSTALL_SCRIPT_INVALID_SCRIPT_VERSION = 'PROCESS_INSTALL_SCRIPT_INVALID_SCRIPT_VERSION', + PROCESS_INSTALL_SCRIPT_INVALID_IF_CONDITION = 'PROCESS_INSTALL_SCRIPT_INVALID_IF_CONDITION', RUN_INSTALL_SCRIPT_UNSUPPORTED_VERSION = 'RUN_INSTALL_SCRIPT_UNSUPPORTED_VERSION', DDP_CLIENT_DISCONNECTED = 'DDP_CLIENT_DISCONNECTED', TASK_FAILED_STALE = "TASK_FAILED_STALE", + UPDATE_APP_NOT_FOUND = 'UPDATE_APP_NOT_FOUND', + UPDATE_APP_NOT_IN_CATALOG = 'UPDATE_APP_NOT_IN_CATALOG', + UPDATE_APP_INVALID_SCRIPT = 'UPDATE_APP_INVALID_SCRIPT', + UPDATE_APP_INVALID_CATALOG_SCRIPT = 'UPDATE_APP_INVALID_CATALOG_SCRIPT', + UPDATE_APP_ANALYSIS_FAILED = 'UPDATE_APP_ANALYSIS_FAILED', + UPDATE_APP_FAILED = 'UPDATE_APP_FAILED', + UPDATE_APP_NO_CURRENT_CONFIG = 'UPDATE_APP_NO_CURRENT_CONFIG', DOMAIN_NOT_FOUND = 'DOMAIN_NOT_FOUND', ACME_ACCOUNT_EMAIL_REQUIRED = 'ACME_ACCOUNT_EMAIL_REQUIRED', ACME_CHALLENGE_FAILED = 'ACME_CHALLENGE_FAILED', ACME_CERTIFICATE_GENERATION_FAILED = 'ACME_CERTIFICATE_GENERATION_FAILED', - } diff --git a/eshtek/routes.ts b/eshtek/routes.ts index c9dc908..b13b785 100644 --- a/eshtek/routes.ts +++ b/eshtek/routes.ts @@ -1,4 +1,5 @@ import { GlobalErrorCode } from './errors'; +import type { AppConfiguration } from './apps'; import type { LocationPreferenceId } from './preferences'; import type { ServerPool, ServerPoolNew, ServerUser, ServerUserType } from './server'; import type { VMBasics, VMSettings } from './vms'; @@ -115,11 +116,62 @@ export interface RequestDockerUpdatePool { poolName: string; } -export interface RequestAppInstall { +export interface RequestAppInstall extends AppConfiguration { id: string; train?: 'community' | 'stable'; - installScript?: string; - questionResponses?: Record; +} + +export interface RequestAppUpdate extends AppConfiguration {} +export interface RequestAppUpdateAnalysis extends AppConfiguration {} + + +export enum ResourceChangeType { + STORAGE = 'storage', + NETWORK = 'network', + ENVIRONMENT = 'environment', + RESOURCE = 'resource', + PERMISSION = 'permission', + CONFIG = 'config' +} + +export enum ResourceChangeAction { + CREATE = 'create', + UPDATE = 'update', + DELETE = 'delete', + NO_OP = 'no-op', + PRESERVE = 'preserve' +} + +export interface ResponseResourceChanges { + resourceChanges: Array<{ + address: string; + type: ResourceChangeType; + change: { + actions: Array; + before: any; + after: any; + }; + }>; + planSummary: { + totalChanges: number; + changesByAction: { + create: number; + update: number; + delete: number; + noOp: number; + preserve: number; + }; + }; + validation: { + updateCompatible: boolean; + compatibilityConstraint?: string; + warnings: string[]; + }; + versionInfo: { + current?: string; + target?: string; + changelog?: string; + }; } export interface RequestAppDelete { diff --git a/eshtek/server-schema.ts b/eshtek/server-schema.ts index 4bb5206..e0463ea 100644 --- a/eshtek/server-schema.ts +++ b/eshtek/server-schema.ts @@ -27,6 +27,8 @@ import { } from "./server"; import { vMSHealthSchema } from "./vms-schema"; +const appsHealthSchema = z.any(); + export const serverUserTypeSchema = z.nativeEnum(ServerUserType); @@ -135,6 +137,12 @@ export const serverSystemDataSystemDeviceDataSchema = z.object({ networking: z.array(serverSystemDataSystemDeviceSchema).optional(), }); +export const serverSystemDataApplicationsSchema = + serverStatusBasicsSchema.extend({ + type: z.literal(ServerStatusType.APPLICATIONS), + health: appsHealthSchema, + }); + export const serverSystemDataVirtualizationSchema = serverStatusBasicsSchema.extend({ type: z.literal(ServerStatusType.VIRTUALIZATION), @@ -220,8 +228,6 @@ const fileTypeSchema = z.any(); const topologyItemStatusSchema = z.any(); -const appsHealthSchema = z.any(); - const networkInterfaceTypeSchema = z.any(); export const serverPoolBasicsSchema = z.object({ @@ -316,12 +322,6 @@ export const serverSystemDataStorageSchema = serverStatusBasicsSchema.extend({ }), }); -export const serverSystemDataApplicationsSchema = - serverStatusBasicsSchema.extend({ - type: z.literal(ServerStatusType.APPLICATIONS), - health: appsHealthSchema, - }); - export const serverSystemDataSchema = z.union([ serverSystemDataSystemSchema, serverSystemDataStorageSchema, diff --git a/eshtek/tasks.ts b/eshtek/tasks.ts index 4d5a83f..3c8d042 100644 --- a/eshtek/tasks.ts +++ b/eshtek/tasks.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ // tasks.ts +import type { ResponseResourceChanges } from './routes'; import type { DiskType } from '../truenas/webui/enums/disk-type.enum'; export interface HexTaskBase { @@ -115,6 +116,27 @@ type HexTaskMeta = + R extends HexTaskAppUpdateReason ? HexTaskAppUpdateDataMap[R] : HexTaskAppUpdateDataMap[HexTaskAppUpdateReason]; + export type HexTaskDataMap = { [HexTaskType.RESTART]: HexTaskMeta; [HexTaskType.SHUTDOWN]: HexTaskMeta; @@ -137,10 +159,10 @@ export type HexTaskDataMap = { [HexTaskType.APP_INSTALL]: HexTaskMeta; [HexTaskType.APP_UNINSTALL]: HexTaskMeta; [HexTaskType.APP_UPGRADE]: HexTaskMeta; - [HexTaskType.APP_UPDATE]: HexTaskMeta; + [HexTaskType.APP_UPDATE]: HexTaskMeta; [HexTaskType.PREFERENCE_LOCATION_PATH_MIGRATION]: HexTaskMeta; [HexTaskType.DRIVE_REPLACE]: HexTaskMeta; - [HexTaskType.DOCKER_UPDATE]: HexTaskMeta; + [HexTaskType.DOCKER_UPDATE]: HexTaskMeta; }; // This looks a little strange with duplicated code, but we need a runtime const avail for the utils file diff --git a/truenas/webui/interfaces/api/api-call-directory.interface.ts b/truenas/webui/interfaces/api/api-call-directory.interface.ts index 1f4bf14..4b59abb 100644 --- a/truenas/webui/interfaces/api/api-call-directory.interface.ts +++ b/truenas/webui/interfaces/api/api-call-directory.interface.ts @@ -204,6 +204,7 @@ import type { SystemHealth } from '../system-health.interface'; import type { App, AppQueryParams, AppUpgradeParams } from '../app.interface'; import type { DetailsDisk, DiskSmartAttribute } from '../disk.interface'; import { DockerConfig, DockerStatusData } from '../../enums/docker-config.interface'; +import type { GpuChoices } from '../gpu.interface'; /** * API definitions for `call` methods. @@ -267,6 +268,7 @@ export interface ApiCallDirectory { 'app.similar': { params: [app_name: string, train: string]; response: AvailableApp[] }; 'app.rollback_versions': { params: [app_name: string]; response: string[] }; 'app.used_ports': { params: void; response: number[] }; + 'app.gpu_choices': { params: void; response: GpuChoices }; // Audit 'audit.config': { params: void; response: AuditConfig }; diff --git a/truenas/webui/interfaces/gpu.interface.ts b/truenas/webui/interfaces/gpu.interface.ts new file mode 100644 index 0000000..0f8342a --- /dev/null +++ b/truenas/webui/interfaces/gpu.interface.ts @@ -0,0 +1,20 @@ +/** + * GPU interface types based on TrueNAS middleware API + * From middlewared/middlewared/api/v25_04_1/app.py + */ + +export interface GpuVendorSpecificConfig { + uuid?: string; + [key: string]: unknown; +} + +export interface GpuDevice { + vendor: string | null; + description: string | null; + error: string | null; + vendor_specific_config: GpuVendorSpecificConfig; + gpu_details: Record; + pci_slot: string | null; +} + +export type GpuChoices = Record; \ No newline at end of file diff --git a/ts-to-zod.config.js b/ts-to-zod.config.js index 4a9c3e9..e02bcdf 100644 --- a/ts-to-zod.config.js +++ b/ts-to-zod.config.js @@ -1,6 +1,6 @@ module.exports = [ { - name: "vms", + name: "apps", input: "eshtek/vms.ts", output: "eshtek/vms-schema.ts", },