chore: update cloudly dependency stack

Align Cloudly with the current typedserver, smartconfig, smartstate, and Docker tooling releases so builds and Docker output stay compatible with the upgraded stack.
This commit is contained in:
2026-05-08 13:56:20 +00:00
parent 80226c8a1c
commit f40ef6b7c0
75 changed files with 4003 additions and 6406 deletions
+55
View File
@@ -6,6 +6,61 @@
},
"platforms": ["linux/amd64", "linux/arm64"]
},
"@git.zone/tsbundle": {
"bundles": [
{
"from": "./ts_web/index.ts",
"to": "./dist_serve/bundle.js",
"outputMode": "bundle",
"bundler": "esbuild",
"production": true,
"includeFiles": ["./html/**/*.html"]
}
]
},
"@git.zone/tswatch": {
"preset": "website",
"server": {
"enabled": true,
"port": 3000,
"serveDir": "./dist_serve/",
"liveReload": true
},
"watchers": [
{
"name": "backend",
"watch": ["./ts/**/*", "./ts_cliclient/**/*"],
"command": "pnpm run startTs",
"restart": true,
"debounce": 300,
"runOnStart": true
}
],
"bundles": [
{
"name": "website",
"from": "./ts_web/index.ts",
"to": "./dist_serve/bundle.js",
"watchPatterns": ["./ts_web/**/*", "./html/**/*"],
"triggerReload": true,
"bundler": "esbuild",
"production": false,
"includeFiles": ["./html/**/*.html"]
}
]
},
"@ship.zone/szci": {
"npmGlobalTools": [],
"npmAccessLevel": "public",
"npmRegistryUrl": "verdaccio.lossless.digital",
"dockerRegistryRepoMap": {
"code.foss.global": "serve.zone/cloudly"
},
"dockerBuildargEnvMap": {}
},
"tsdoc": {
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
},
"@git.zone/cli": {
"projectType": "service",
"module": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"json.schemas": [
{
"fileMatch": ["/npmextra.json"],
"fileMatch": ["/.smartconfig.json"],
"schema": {
"type": "object",
"properties": {
-14
View File
@@ -1,14 +0,0 @@
{
"@ship.zone/szci": {
"npmGlobalTools": [],
"npmAccessLevel": "public",
"npmRegistryUrl": "verdaccio.lossless.digital",
"dockerRegistryRepoMap": {
"code.foss.global": "serve.zone/cloudly"
},
"dockerBuildargEnvMap": {}
},
"tsdoc": {
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
}
}
+53 -58
View File
@@ -1,7 +1,7 @@
{
"name": "@serve.zone/cloudly",
"version": "5.4.0",
"private": false,
"version": "5.5.1",
"private": true,
"description": "A comprehensive tool for managing containerized applications across multiple cloud providers using Docker Swarmkit, featuring web, CLI, and API interfaces.",
"type": "module",
"exports": {
@@ -13,86 +13,86 @@
"license": "MIT",
"scripts": {
"test": "(tstest test/ --verbose --logfile --timeout 120)",
"build": "tsbuild tsfolders --web --allowimplicitany && tsbundle website --production",
"build": "tsbuild tsfolders && tsbundle",
"build:docker": "tsdocker build --verbose",
"start": "node cli.js",
"startTs": "node cli.ts.js",
"watch": "tswatch website",
"watch": "tswatch",
"release:docker": "tsdocker push --verbose",
"publish": "tspublish",
"docs": "tsdoc aidoc"
},
"devDependencies": {
"@git.zone/tsbuild": "^2.6.8",
"@git.zone/tsbundle": "^2.5.1",
"@git.zone/tsdocker": "^2.2.4",
"@git.zone/tsdoc": "^1.5.2",
"@git.zone/tspublish": "^1.10.3",
"@git.zone/tstest": "^2.3.8",
"@git.zone/tswatch": "^2.2.1",
"@push.rocks/smartnetwork": "^4.4.0",
"@types/node": "^22.0.0"
"@git.zone/tsbuild": "^4.4.0",
"@git.zone/tsbundle": "^2.10.1",
"@git.zone/tsdoc": "^2.0.3",
"@git.zone/tsdocker": "^2.2.6",
"@git.zone/tspublish": "^1.11.6",
"@git.zone/tstest": "^3.6.5",
"@git.zone/tswatch": "^3.3.3",
"@push.rocks/smartnetwork": "^4.7.1",
"@types/node": "^25.6.2"
},
"dependencies": {
"@api.global/typedrequest": "3.1.10",
"@api.global/typedrequest": "3.3.1",
"@api.global/typedrequest-interfaces": "^3.0.19",
"@api.global/typedserver": "^3.0.79",
"@api.global/typedsocket": "^3.0.1",
"@apiclient.xyz/cloudflare": "^6.4.1",
"@apiclient.xyz/docker": "^1.3.5",
"@apiclient.xyz/hetznercloud": "^1.2.0",
"@api.global/typedserver": "^8.4.6",
"@api.global/typedsocket": "^4.1.3",
"@apiclient.xyz/cloudflare": "^7.1.0",
"@apiclient.xyz/docker": "^5.1.4",
"@apiclient.xyz/hetznercloud": "^1.2.1",
"@apiclient.xyz/slack": "^3.0.9",
"@design.estate/dees-catalog": "^1.11.3",
"@design.estate/dees-domtools": "^2.3.3",
"@design.estate/dees-element": "^2.1.2",
"@git.zone/tsrun": "^1.3.3",
"@push.rocks/early": "^4.0.3",
"@push.rocks/npmextra": "^5.3.3",
"@push.rocks/projectinfo": "^5.0.1",
"@push.rocks/qenv": "^6.1.3",
"@push.rocks/smartacme": "^8.0.0",
"@push.rocks/smartbucket": "^3.3.10",
"@push.rocks/smartcli": "^4.0.11",
"@push.rocks/smartclickhouse": "^2.0.17",
"@push.rocks/smartdata": "^5.16.4",
"@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartexit": "^1.0.23",
"@design.estate/dees-catalog": "^3.81.0",
"@design.estate/dees-domtools": "^2.5.6",
"@design.estate/dees-element": "^2.2.4",
"@git.zone/tsrun": "^2.0.3",
"@push.rocks/early": "^4.0.4",
"@push.rocks/projectinfo": "^5.1.0",
"@push.rocks/qenv": "^6.1.4",
"@push.rocks/smartacme": "^9.5.0",
"@push.rocks/smartbucket": "^4.6.1",
"@push.rocks/smartcli": "^4.0.21",
"@push.rocks/smartclickhouse": "^2.2.1",
"@push.rocks/smartconfig": "^6.1.1",
"@push.rocks/smartdata": "^7.1.7",
"@push.rocks/smartdelay": "^3.1.0",
"@push.rocks/smartexit": "^2.0.3",
"@push.rocks/smartexpect": "^2.5.0",
"@push.rocks/smartfile": "^11.2.7",
"@push.rocks/smartfile": "^13.1.3",
"@push.rocks/smartguard": "^3.1.0",
"@push.rocks/smartjson": "^5.2.0",
"@push.rocks/smartjson": "^6.0.1",
"@push.rocks/smartjwt": "^2.2.2",
"@push.rocks/smartlog": "^3.1.9",
"@push.rocks/smartlog": "^3.2.2",
"@push.rocks/smartlog-destination-clickhouse": "^1.0.13",
"@push.rocks/smartlog-interfaces": "^3.0.2",
"@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartregistry": "^2.9.1",
"@push.rocks/smartrequest": "^4.3.1",
"@push.rocks/smartsamba": "^0.2.0",
"@push.rocks/smartpromise": "^4.2.4",
"@push.rocks/smartregistry": "^2.9.2",
"@push.rocks/smartrequest": "^5.0.3",
"@push.rocks/smartrx": "^3.0.10",
"@push.rocks/smartssh": "^2.0.1",
"@push.rocks/smartstate": "^2.0.27",
"@push.rocks/smartstream": "^3.2.5",
"@push.rocks/smartstring": "^4.1.0",
"@push.rocks/smartsamba": "^0.2.0",
"@push.rocks/smartssh": "^2.1.0",
"@push.rocks/smartstate": "^2.3.1",
"@push.rocks/smartstream": "^3.4.2",
"@push.rocks/smartstring": "^4.1.1",
"@push.rocks/smartunique": "^3.0.9",
"@push.rocks/taskbuffer": "^3.4.0",
"@push.rocks/webjwt": "^1.0.9",
"@serve.zone/api": "^5.3.4",
"@serve.zone/interfaces": "^5.5.0",
"@tsclass/tsclass": "^9.2.0"
"@push.rocks/taskbuffer": "^8.0.2",
"@push.rocks/webjwt": "^1.0.10",
"@serve.zone/api": "^5.3.7",
"@serve.zone/interfaces": "^5.6.0",
"@tsclass/tsclass": "^9.5.1"
},
"files": [
"ts/**/*",
"ts_cliclient/**/*",
"ts_web/**/*",
"dist/**/*",
"dist_*/**/*",
"dist_serve/**/*",
"dist_ts/**/*",
"dist_ts_cliclient/**/*",
"dist_ts_web/**/*",
"assets/**/*",
"cli.js",
".smartconfig.json",
"npmextra.json",
"readme.md"
],
"browserslist": [
@@ -134,10 +134,5 @@
"backend",
"security"
],
"pnpm": {
"overrides": {
"@push.rocks/lik": "6.2.2"
}
},
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
}
+3201 -5800
View File
File diff suppressed because it is too large Load Diff
+54 -6
View File
@@ -30,15 +30,17 @@ Cloudly currently coordinates these areas:
- **Authentication and identity**: human admin login, JWT identities, machine tokens, and cluster identities.
- **Clusters**: desired cluster records and machine users used by Coreflow to authenticate back to Cloudly.
- **Services**: workload definitions, image references, domains, ports, scale factors, secret bundles, volumes, and deployment metadata.
- **Deployments**: deployment records, node placement metadata, health/resource fields, restart/scale API stubs, and DNS activation/deactivation hooks.
- **Images and registries**: image metadata, S3-backed image storage, external registry records, and an embedded OCI registry mounted at `/v2`.
- **Secrets**: secret groups and bundles that Coreflow flattens into Docker secrets for workloads.
- **Domains and DNS**: domain records, DNS entries, and optional domain sync from a dcrouter external gateway.
- **Platform bindings**: first-class `database` and `objectstorage` bindings that Coreflow provisions through Corestore.
- **Platform bindings**: capabilities such as `database`, `objectstorage`, `logging`, `backup`, and RPC-style platform services that Coreflow/Corestore can reconcile.
- **Backups**: backup records, service backup/restore requests, scheduled backup tasks, and archive replication handshakes with Coreflow/Corestore.
- **BaseOS**: managed BaseOS node registration, heartbeat handling, desired-state response, image build tracking, and image download URLs.
- **CoreBuild workers**: selection of external build workers for BaseOS ISO/raw-image artifact generation.
- **CoreBuild workers**: selection of external build workers for BaseOS ISO and balena raw-image artifact generation.
- **Tasks**: TaskBuffer-backed operational tasks with execution history, metrics, logs, manual triggers, cancellation, and cron schedules.
- **Node and bare-metal inventory**: Hetzner-backed node creation paths and bare-metal metadata records where configured.
- **Dashboard**: a web component UI rendered from `ts_web` with views for clusters, services, images, secrets, domains, DNS, tasks, backups, BaseOS, and settings.
- **Dashboard**: a web component UI rendered from `ts_web` with views for overview, settings, secrets, clusters, external registries, images, services, deployments, tasks, domains, DNS, mail/log/storage/database shells, backups, and BaseOS.
## Runtime Components
@@ -53,10 +55,12 @@ Cloudly currently coordinates these areas:
| `CloudlyRegistryManager` | Embedded OCI registry backed by configured S3 storage, including deploy-on-push metadata updates. |
| `CloudlyBaseOsManager` | BaseOS registration, heartbeat, image build orchestration, worker selection, and artifact downloads. |
| `CloudlyBackupManager` | Service backup/restore orchestration and remote archive object replication. |
| `CloudlyTaskManager` | Registers predefined and runtime tasks, tracks task executions, schedules cron jobs, and exposes task APIs. |
| `CloudlySettingsManager` | Stores runtime settings in MongoDB, masks sensitive values for API responses, and refreshes gateway/Coreflow state after relevant changes. |
## Configuration
Cloudly uses `@push.rocks/npmextra` app data and environment mappings. Values can be supplied through environment variables, `.nogit` app-data files, or by constructing `new Cloudly(config)` programmatically.
Cloudly uses `@push.rocks/smartconfig` `AppData` with environment mappings. The runtime entry point loads `.nogit`/environment values through `@push.rocks/qenv`, and embedded callers can override values by constructing `new Cloudly(config)` programmatically.
Required runtime configuration:
@@ -88,6 +92,23 @@ Common optional settings are stored through the Cloudly settings manager rather
| `corebuildWorkersJson` | JSON array of CoreBuild worker URLs or `{ "url", "token", "id" }` objects. |
| `corebuildWorkerUrl` / `corebuildWorkerToken` | Legacy single-worker CoreBuild settings. |
| `dcrouterGatewayUrl` / `dcrouterGatewayApiToken` | Optional external gateway integration for domain and route sync. |
| `dcrouterWorkHosterId` | Optional stable external gateway work hoster ID; defaults to the cluster ID. |
| `dcrouterTargetHost` / `dcrouterTargetPort` | Optional target address that dcrouter should forward workload traffic to. |
Optional runtime environment variables:
| Variable | Purpose |
| --- | --- |
| `SERVEZONE_INSTALL_DEMO_DATA` | Runs the destructive demo data installer when set to `true`. |
| `CLOUDLY_BACKUP_CRON` | Enables the scheduled `backup-all-services` task with the supplied cron expression. |
| `CLOUDLY_BACKUP_KEEP_LAST` | Number of completed/failed backups to retain per service; defaults to `24`. |
| `CLOUDLY_BACKUP_TARGET_TYPE` | Remote archive replication target, currently `s3` or `smb`. |
| `CLOUDLY_BACKUP_TARGET_PREFIX` | Remote backup path prefix; defaults to `serve.zone-backups`. |
| `CLOUDLY_BASEOS_IMAGE_CLEANUP_INTERVAL_MS` | BaseOS image artifact cleanup interval; defaults to 12 hours. |
For backup replication with `CLOUDLY_BACKUP_TARGET_TYPE=s3`, set `CLOUDLY_BACKUP_S3_ENDPOINT`, `CLOUDLY_BACKUP_S3_ACCESS_KEY`, `CLOUDLY_BACKUP_S3_SECRET_KEY`, and `CLOUDLY_BACKUP_S3_BUCKET`. Optional S3 variables are `CLOUDLY_BACKUP_S3_REGION`, `CLOUDLY_BACKUP_S3_PORT`, and `CLOUDLY_BACKUP_S3_USE_SSL`.
For backup replication with `CLOUDLY_BACKUP_TARGET_TYPE=smb`, set `CLOUDLY_BACKUP_SMB_HOST` and `CLOUDLY_BACKUP_SMB_SHARE`. Optional SMB variables are `CLOUDLY_BACKUP_SMB_PORT`, `CLOUDLY_BACKUP_SMB_USERNAME`, `CLOUDLY_BACKUP_SMB_PASSWORD`, and `CLOUDLY_BACKUP_SMB_DOMAIN`.
## Starting Cloudly
@@ -141,6 +162,8 @@ Set `SERVEZONE_INSTALL_DEMO_DATA=true` only when you intentionally want the demo
Cloudly exposes a single composed TypedRouter. Managers add their own typed handlers to the main router, and `CloudlyServer` exposes that router through the HTTP/WebSocket server.
On first startup, Cloudly bootstraps the first human admin from `SERVEZONE_ADMINACCOUNT`. Human clients authenticate through `adminLoginWithUsernameAndPassword`; machine clients authenticate through `getIdentityByToken`. Cluster creation creates a machine user and token for Coreflow.
Typical consumers use `@serve.zone/api`:
```ts
@@ -178,11 +201,13 @@ Cloudly serves an OCI registry under `/v2` through `CloudlyRegistryManager`. The
For Cloudly-managed services, `getServiceRegistryTarget()` creates stable registry targets like:
```text
<cloudly-host>/workloads/<service-name>-<service-id>:<tag>
<cloudly-host>/workloads/<service-name>-<service-id-prefix>:<tag>
```
Registry push hooks record tag/digest metadata on the linked image and service. Unless `deployOnPush` is explicitly `false`, a successful push updates the service image version and asks connected Coreflow clients to reconcile.
Registry token requests use HTTP Basic credentials against Cloudly users. User passwords and unexpired user tokens are accepted; push/delete scopes require an admin user or a token with the `admin` assigned role.
## BaseOS and CoreBuild
Cloudly can manage BaseOS nodes and image builds:
@@ -191,6 +216,8 @@ Cloudly can manage BaseOS nodes and image builds:
- A configured `baseosJoinToken` accepts generic device enrollment.
- BaseOS image builds create one-time provisioning tokens that are embedded in generated images.
- Cloudly selects a CoreBuild worker based on `/corebuild/v1/capabilities` and sends the build to `/corebuild/v1/jobs/baseos-image`.
- Supported build kinds are `ubuntu-iso` and `balena-raw`; Raspberry Pi builds use `balena-raw`.
- Supported architecture values are `amd64`, `arm64`, and `rpi`.
- Completed artifacts are stored in the configured S3 bucket and served through short-lived `/baseos/v1/images/:buildId/download` URLs.
CoreBuild worker configuration can use `corebuildWorkersJson` for multiple workers or the legacy `corebuildWorkerUrl` and `corebuildWorkerToken` settings for one worker.
@@ -207,6 +234,27 @@ The backup path includes:
- Optional archive replication through `prepareBackupReplication`, `uploadBackupArchiveObject`, `completeBackupReplication`, `getBackupArchiveManifest`, and `downloadBackupArchiveObject`.
- Optional scheduled `backup-all-services` task when `CLOUDLY_BACKUP_CRON` is set.
Manual `createServiceBackup` requests expect Coreflow to complete remote archive replication. Cloudly validates archive object size and SHA-256 checksums, writes a manifest, records target metadata, and marks completed backups as `replicated`. Restores read the manifest and objects back through the configured target writer.
## Task Automation
Cloudly registers a TaskBuffer-backed task manager. The API and dashboard can list tasks, trigger tasks manually, inspect execution logs/metrics, and request cancellation for running tasks.
Predefined tasks currently include:
| Task | Purpose |
| --- | --- |
| `cloudflare-domain-sync` | Imports and updates domains from configured Cloudflare zones. |
| `dns-sync` | Iterates DNS entries marked as external; provider sync is currently a placeholder. |
| `cert-renewal` | Checks activated domains for certificate renewal; renewal logic is currently a placeholder. |
| `cleanup` | Removes old task executions and contains placeholders for log/image cleanup. |
| `health-check` | Iterates deployments and records health metrics; runtime health checks are currently placeholders. |
| `resource-report` | Generates node resource metrics; values are currently placeholders until runtime metrics are wired in. |
| `db-maintenance` | Maintenance shell for database optimization tasks. |
| `security-scan` | Security scan shell for exposed ports, image freshness, and weak configuration checks. |
| `docker-cleanup` | Docker cleanup shell for containers, images, volumes, and networks. |
| `backup-all-services` | Registered by the backup manager and enabled only when `CLOUDLY_BACKUP_CRON` is set. |
## External Gateway Integration
Cloudly can integrate with a dcrouter gateway when the gateway URL and API token are present in settings. The current integration syncs externally available domains into Cloudly and passes an external gateway route configuration to Coreflow. Coreflow can then ask dcrouter for certificates and synchronize public routes while still routing to cluster-local Coretraffic.
@@ -238,7 +286,7 @@ Important paths:
## Accuracy Notes
The package metadata and settings schema include fields for several cloud providers. The code paths currently exercised in this repository are Cloudflare for ACME DNS-01, Hetzner for selected node/bare-metal provisioning paths, S3-compatible storage, MongoDB/SmartData, CoreBuild, Coreflow, Corestore, and optional dcrouter integration. Verify provider-specific behavior in the relevant manager before relying on it operationally.
The package metadata and settings schema include fields for several cloud providers. The code paths currently exercised in this repository are Cloudflare for ACME DNS-01 and domain sync, Hetzner for selected node/bare-metal provisioning paths, S3-compatible storage, SMB/S3 backup archive targets, MongoDB/SmartData, CoreBuild, Coreflow, Corestore, and optional dcrouter integration. Several provider connection tests and predefined tasks are configuration checks or implementation shells; verify provider-specific behavior in the relevant manager before relying on it operationally.
## License and Legal Information
+8 -10
View File
@@ -1,4 +1,4 @@
import { expect, tap } from '@push.rocks/tapbundle';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { Qenv } from '@push.rocks/qenv';
const testQenv = new Qenv('./', './.nogit/');
@@ -12,17 +12,15 @@ let testCloudly: cloudly.Cloudly;
tap.test('first test', async () => {
const cloudlyConfig: cloudly.ICloudlyConfig = {
cfToken: await testQenv.getEnvVarOnDemand('CF_TOKEN'),
environment: 'integration',
letsEncryptEmail: await testQenv.getEnvVarOnDemand('LETSENCRYPT_EMAIL'),
publicUrl: await testQenv.getEnvVarOnDemand('SERVEZONE_URL'),
publicPort: await testQenv.getEnvVarOnDemand('SERVEZONE_PORT'),
letsEncryptEmail: await testQenv.getEnvVarOnDemandStrict('LETSENCRYPT_EMAIL'),
publicUrl: await testQenv.getEnvVarOnDemandStrict('SERVEZONE_URL'),
publicPort: await testQenv.getEnvVarOnDemandStrict('SERVEZONE_PORT'),
mongoDescriptor: {
mongoDbName: await testQenv.getEnvVarOnDemand('MONGODB_DATABASE'),
mongoDbPass: await testQenv.getEnvVarOnDemand('MONGODB_PASSWORD'),
mongoDbUrl: await testQenv.getEnvVarOnDemand('MONGODB_URL')
mongoDbName: await testQenv.getEnvVarOnDemandStrict('MONGODB_DATABASE'),
mongoDbPass: await testQenv.getEnvVarOnDemandStrict('MONGODB_PASSWORD'),
mongoDbUrl: await testQenv.getEnvVarOnDemandStrict('MONGODB_URL')
},
digitalOceanToken: await testQenv.getEnvVarOnDemand('DIGITALOCEAN_TOKEN')
};
testCloudly = new cloudly.Cloudly(cloudlyConfig);
expect(testCloudly).toBeInstanceOf(cloudly.Cloudly);
@@ -36,4 +34,4 @@ tap.test('should end the service', async () => {
await testCloudly.stop();
});
tap.start();
export default tap.start();
+10 -12
View File
@@ -1,10 +1,10 @@
import { expect, tap } from '@push.rocks/tapbundle';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { Qenv } from '@push.rocks/qenv';
const testQenv = new Qenv('./', './.nogit/');
delete process.env.CLI_CALL;
import * as cloudly from '../ts/index';
import * as cloudly from '../ts/index.js';
process.env.TESTING_CLOUDLY = 'true';
@@ -12,20 +12,18 @@ let testCloudly: cloudly.Cloudly;
tap.test('first test', async () => {
const cloudlyConfig: cloudly.ICloudlyConfig = {
cfToken: testQenv.getEnvVarOnDemand('CF_TOKEN'),
environment: 'integration',
letsEncryptEmail: testQenv.getEnvVarOnDemand('LETSENCRYPT_EMAIL'),
publicUrl: testQenv.getEnvVarOnDemand('SERVEZONE_URL'),
publicPort: testQenv.getEnvVarOnDemand('SERVEZONE_PORT'),
letsEncryptEmail: await testQenv.getEnvVarOnDemandStrict('LETSENCRYPT_EMAIL'),
publicUrl: await testQenv.getEnvVarOnDemandStrict('SERVEZONE_URL'),
publicPort: await testQenv.getEnvVarOnDemandStrict('SERVEZONE_PORT'),
mongoDescriptor: {
mongoDbName: testQenv.getEnvVarOnDemand('MONGODB_DATABASE'),
mongoDbPass: testQenv.getEnvVarOnDemand('MONGODB_PASSWORD'),
mongoDbUrl: testQenv.getEnvVarOnDemand('MONGODB_URL')
mongoDbName: await testQenv.getEnvVarOnDemandStrict('MONGODB_DATABASE'),
mongoDbPass: await testQenv.getEnvVarOnDemandStrict('MONGODB_PASSWORD'),
mongoDbUrl: await testQenv.getEnvVarOnDemandStrict('MONGODB_URL')
},
digitalOceanToken: testQenv.getEnvVarOnDemand('DIGITALOCEAN_TOKEN')
};
testCloudly = new cloudly.Cloudly(cloudlyConfig);
expect(testCloudly).to.be.instanceof(cloudly.Cloudly);
expect(testCloudly).toBeInstanceOf(cloudly.Cloudly);
});
tap.test('should init servezone', async () => {
@@ -36,4 +34,4 @@ tap.test('should end the service', async () => {
await testCloudly.stop();
});
tap.start();
export default tap.start();
+8 -6
View File
@@ -1,6 +1,9 @@
import { Qenv } from '@push.rocks/qenv';
import { SmartNetwork } from '@push.rocks/smartnetwork';
import { tapNodeTools } from '@git.zone/tstest/tapbundle_node';
import { tap } from '@git.zone/tstest/tapbundle';
import { TapNodeTools } from '@git.zone/tstest/tapbundle_serverside';
const tapNodeTools = new TapNodeTools(tap);
const testQenv = new Qenv('./', './.nogit/');
@@ -24,10 +27,10 @@ const smartmongo = await tapNodeTools.createSmartmongo();
stopFunctions.push(async () => {
await smartmongo.stopAndDumpToDir('./.nogit/mongodump');
});
const smarts3 = await tapNodeTools.createSmarts3();
await smarts3.createBucket('cloudly_test_bucket');
const smartstorage = await tapNodeTools.createSmartStorage();
await smartstorage.createBucket('cloudly_test_bucket');
stopFunctions.push(async () => {
await smarts3.stop();
await smartstorage.stop();
});
export const testCloudlyAdminAccount = {
@@ -36,13 +39,12 @@ export const testCloudlyAdminAccount = {
};
export const testCloudlyConfig: cloudly.ICloudlyConfig = {
cfToken: await testQenv.getEnvVarOnDemand('CF_TOKEN'),
environment: 'integration',
letsEncryptEmail: 'test@serve.zone',
publicUrl: '127.0.0.1',
publicPort: await getPublicPort(),
mongoDescriptor: await smartmongo.getMongoDescriptor(),
s3Descriptor: await smarts3.getS3Descriptor({
s3Descriptor: await smartstorage.getStorageDescriptor({
bucketName: 'cloudly_test_bucket'
}),
sslMode: 'none',
+13 -7
View File
@@ -7,6 +7,15 @@ import * as cloudlyApiClient from '@serve.zone/api';
let testCloudly: cloudly.Cloudly;
let testClient: cloudlyApiClient.CloudlyApiClient;
const logErrorDetails = (errorArg: unknown) => {
if (errorArg instanceof Error) {
console.error(` - Error message: ${errorArg.message}`);
console.error(` - Error stack:`, errorArg.stack);
return;
}
console.error(` - Error:`, errorArg);
};
tap.preTask('should start cloudly', async () => {
testCloudly = await helpers.createCloudly();
await testCloudly.start();
@@ -31,7 +40,7 @@ tap.preTask('should create a new machine user for testing', async () => {
console.log(` - Username: ${machineUser.data.username}`);
console.log(` - Role: ${machineUser.data.role}`);
console.log(` - Token: 'test'`);
console.log(` - Token roles: ${machineUser.data.tokens[0].assignedRoles}`);
console.log(` - Token roles: ${machineUser.data.tokens?.[0]?.assignedRoles?.join(', ') ?? ''}`);
await machineUser.save();
console.log('✅ PreTask: First machine user saved successfully');
});
@@ -78,8 +87,7 @@ tap.test('should get an identity', async () => {
expect(identity).toBeTruthy();
} catch (error) {
console.error('❌ Failed to get identity:');
console.error(` - Error message: ${error.message}`);
console.error(` - Error stack:`, error.stack);
logErrorDetails(error);
throw error;
}
});
@@ -378,8 +386,7 @@ tap.test('should create and upload an image', async () => {
expect(image).toBeTruthy();
} catch (error) {
console.error('❌ Failed to create image:');
console.error(` - Error message: ${error.message}`);
console.error(` - Error stack:`, error.stack);
logErrorDetails(error);
throw error;
}
})
@@ -398,8 +405,7 @@ tap.test('should upload an image version', async () => {
console.log('✅ Image version uploaded successfully');
} catch (error) {
console.error('❌ Failed to upload image version:');
console.error(` - Error message: ${error.message}`);
console.error(` - Error stack:`, error.stack);
logErrorDetails(error);
throw error;
}
});
+1 -1
View File
@@ -20,4 +20,4 @@ tap.test('should end the service', async (tools) => {
tools.delayFor(1000).then(() => process.exit());
});
tap.start();
export default tap.start();
+7 -3
View File
@@ -82,7 +82,7 @@ export class Cloudly {
private readyDeferred = new plugins.smartpromise.Deferred();
private configOptions: plugins.servezoneInterfaces.data.ICloudlyConfig;
private configOptions?: plugins.servezoneInterfaces.data.ICloudlyConfig;
constructor(configArg?: plugins.servezoneInterfaces.data.ICloudlyConfig) {
this.configOptions = configArg;
this.cloudlyInfo = new CloudlyInfo(this);
@@ -148,7 +148,9 @@ export class Cloudly {
await this.domainManager.init();
await this.cloudflareConnector.init();
await this.letsencryptConnector.init();
if (this.config.data.sslMode === 'letsencrypt') {
await this.letsencryptConnector.init();
}
await this.clusterManager.init();
await this.server.start();
this.readyDeferred.resolve();
@@ -163,7 +165,9 @@ export class Cloudly {
*/
public async stop() {
await this.server.stop();
await this.letsencryptConnector.stop();
if (this.config.data.sslMode === 'letsencrypt') {
await this.letsencryptConnector.stop();
}
await this.mongodbConnector.stop();
await this.secretManager.stop();
await this.serviceManager.stop();
+1 -1
View File
@@ -9,5 +9,5 @@ export class CloudlyInfo {
this.cloudlyRef = cloudlyRefArg;
}
public projectInfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
public projectInfo = plugins.projectinfo.ProjectInfo.create(paths.packageDir);
}
+5 -4
View File
@@ -8,8 +8,8 @@ import type { Cloudly } from './classes.cloudly.js';
*/
export class CloudlyConfig {
public cloudlyRef: Cloudly;
public appData: plugins.npmextra.AppData<plugins.servezoneInterfaces.data.ICloudlyConfig>;
public data: plugins.servezoneInterfaces.data.ICloudlyConfig;
public appData!: plugins.smartconfig.AppData<plugins.servezoneInterfaces.data.ICloudlyConfig>;
public data!: plugins.servezoneInterfaces.data.ICloudlyConfig;
constructor(cloudlyRefArg: Cloudly) {
this.cloudlyRef = cloudlyRefArg;
@@ -17,12 +17,12 @@ export class CloudlyConfig {
public async init(configArg?: plugins.servezoneInterfaces.data.ICloudlyConfig) {
this.appData =
await plugins.npmextra.AppData.createAndInit<plugins.servezoneInterfaces.data.ICloudlyConfig>(
await plugins.smartconfig.AppData.createAndInit<plugins.servezoneInterfaces.data.ICloudlyConfig>(
{
envMapping: {
environment: 'SERVEZONE_ENVIRONMENT' as 'production' | 'integration',
letsEncryptEmail: 'hard:domains@lossless.org',
letsEncryptPrivateKey: null,
letsEncryptPrivateKey: undefined,
publicUrl: 'SERVEZONE_URL',
publicPort: 'SERVEZONE_PORT',
mongoDescriptor: {
@@ -50,6 +50,7 @@ export class CloudlyConfig {
'sslMode',
'environment',
'mongoDescriptor',
's3Descriptor',
],
overwriteObject: configArg,
},
+30 -48
View File
@@ -11,13 +11,13 @@ export class CloudlyServer {
* a reference to the cloudly instance
*/
public cloudlyRef: Cloudly;
public additionalHandlers: plugins.typedserver.servertools.Handler[] = [];
public additionalHandlers: plugins.typedserver.IRouteHandler[] = [];
/**
* the smartexpress server handling the actual requests
*/
public typedServer: plugins.typedserver.TypedServer;
public typedsocketServer: plugins.typedsocket.TypedSocket;
public typedServer!: plugins.typedserver.TypedServer;
public typedsocketServer!: plugins.typedsocket.TypedSocket;
/**
* typedrouter
@@ -39,13 +39,13 @@ export class CloudlyServer {
*/
public async start() {
logger.log('info', `cloudly domain is ${this.cloudlyRef.config.data.publicUrl}`);
let sslCert: plugins.smartacme.Cert;
let sslCert: plugins.smartacme.Cert | undefined;
if (this.cloudlyRef.config.data.sslMode === 'letsencrypt') {
logger.log('info', `Using letsencrypt for ssl mode. Trying to obtain a certificate...`);
logger.log('info', `This might take 10 minutes...`);
sslCert = await this.cloudlyRef.letsencryptConnector.getCertificateForDomain(
this.cloudlyRef.config.data.publicUrl,
this.cloudlyRef.config.data.publicUrl!,
);
logger.log(
'success',
@@ -58,23 +58,6 @@ export class CloudlyServer {
);
}
interface IRequestGuardData {
req: plugins.typedserver.Request;
res: plugins.typedserver.Response;
}
// guards
const guardIp = new plugins.smartguard.Guard<IRequestGuardData>(async (dataArg) => {
if (true) {
return true;
} else {
dataArg.res.status(500);
dataArg.res.send(`Not allowed to perform this operation!`);
dataArg.res.end();
return false;
}
});
// server
this.typedServer = new plugins.typedserver.TypedServer({
cors: true,
@@ -89,43 +72,42 @@ export class CloudlyServer {
injectReload: true,
serveDir: paths.distServeDir,
watch: true,
enableCompression: true,
preferredCompressionMethod: 'gzip',
compression: {
enabled: true,
algorithms: ['gzip'],
},
});
this.typedsocketServer = this.typedServer.typedsocket;
this.typedServer.typedrouter.addTypedRouter(this.typedrouter);
this.typedServer.server.addRoute(
this.typedServer.addRoute(
'/v2',
new plugins.typedserver.servertools.Handler('ALL', async (req, res) => {
await this.cloudlyRef.registryManager.handleHttpRequest(req, res);
}),
'ALL',
async (ctx) => this.cloudlyRef.registryManager.handleHttpRequest(ctx),
);
this.typedServer.server.addRoute(
'/v2/{*splat}',
new plugins.typedserver.servertools.Handler('ALL', async (req, res) => {
await this.cloudlyRef.registryManager.handleHttpRequest(req, res);
}),
this.typedServer.addRoute(
'/v2/*',
'ALL',
async (ctx) => this.cloudlyRef.registryManager.handleHttpRequest(ctx),
);
this.typedServer.server.addRoute(
this.typedServer.addRoute(
'/curlfresh/:scriptname',
this.cloudlyRef.nodeManager.curlfreshInstance.handler,
'ALL',
async (ctx) => this.cloudlyRef.nodeManager.curlfreshInstance.handleRequest(ctx),
);
this.typedServer.server.addRoute(
this.typedServer.addRoute(
'/baseos/v1/nodes/register',
new plugins.typedserver.servertools.Handler('POST', async (req, res) => {
await this.cloudlyRef.baseOsManager.handleRegisterHttpRequest(req, res);
}),
'POST',
async (ctx) => this.cloudlyRef.baseOsManager.handleRegisterHttpRequest(ctx),
);
this.typedServer.server.addRoute(
this.typedServer.addRoute(
'/baseos/v1/nodes/heartbeat',
new plugins.typedserver.servertools.Handler('POST', async (req, res) => {
await this.cloudlyRef.baseOsManager.handleHeartbeatHttpRequest(req, res);
}),
'POST',
async (ctx) => this.cloudlyRef.baseOsManager.handleHeartbeatHttpRequest(ctx),
);
this.typedServer.server.addRoute(
this.typedServer.addRoute(
'/baseos/v1/images/:buildId/download',
new plugins.typedserver.servertools.Handler('GET', async (req, res) => {
await this.cloudlyRef.baseOsManager.handleImageDownloadHttpRequest(req, res);
}),
'GET',
async (ctx) => this.cloudlyRef.baseOsManager.handleImageDownloadHttpRequest(ctx),
);
await this.typedServer.start();
}
@@ -134,6 +116,6 @@ export class CloudlyServer {
* stop the reception instance
*/
public async stop() {
await this.typedServer.stop();
await this.typedServer?.stop();
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ import { Cloudly } from '../classes.cloudly.js';
*/
export class CloudflareConnector {
private cloudlyRef: Cloudly;
public cloudflare: plugins.cloudflare.CloudflareAccount;
public cloudflare?: plugins.cloudflare.CloudflareAccount;
constructor(cloudlyArg: Cloudly) {
this.cloudlyRef = cloudlyArg;
+9 -5
View File
@@ -3,7 +3,7 @@ import { Cloudly } from '../classes.cloudly.js';
export class LetsencryptConnector {
private cloudlyRef: Cloudly;
private smartacme: plugins.smartacme.SmartAcme;
private smartacme!: plugins.smartacme.SmartAcme;
constructor(cloudlyArg: Cloudly) {
this.cloudlyRef = cloudlyArg;
@@ -18,6 +18,10 @@ export class LetsencryptConnector {
* inits letsencrypt
*/
public async init() {
if (!this.cloudlyRef.cloudflareConnector.cloudflare) {
throw new Error('Cloudflare token is required for letsencrypt DNS-01 challenges');
}
// Create DNS-01 challenge handler using Cloudflare
const dnsHandler = new plugins.smartacme.handlers.Dns01Handler(
this.cloudlyRef.cloudflareConnector.cloudflare
@@ -25,13 +29,13 @@ export class LetsencryptConnector {
// Create MongoDB certificate manager
const certManager = new plugins.smartacme.certmanagers.MongoCertManager(
this.cloudlyRef.config.data.mongoDescriptor
this.cloudlyRef.config.data.mongoDescriptor!
);
this.smartacme = new plugins.smartacme.SmartAcme({
accountEmail: this.cloudlyRef.config.data.letsEncryptEmail,
accountEmail: this.cloudlyRef.config.data.letsEncryptEmail!,
accountPrivateKey: this.cloudlyRef.config.data.letsEncryptPrivateKey,
environment: this.cloudlyRef.config.data.environment,
environment: this.cloudlyRef.config.data.environment!,
certManager: certManager,
challengeHandlers: [dnsHandler],
});
@@ -45,6 +49,6 @@ export class LetsencryptConnector {
* stops the instance
*/
public async stop() {
await this.smartacme.stop();
await this.smartacme?.stop();
}
}
+2 -2
View File
@@ -4,7 +4,7 @@ import { Cloudly } from '../classes.cloudly.js';
export class MongodbConnector {
// INSTANCE
private cloudlyRef: Cloudly;
public smartdataDb: plugins.smartdata.SmartdataDb;
public smartdataDb!: plugins.smartdata.SmartdataDb;
constructor(cloudlyRefArg: Cloudly) {
this.cloudlyRef = cloudlyRefArg;
@@ -12,7 +12,7 @@ export class MongodbConnector {
public async init() {
this.smartdataDb = new plugins.smartdata.SmartdataDb(
this.cloudlyRef.config.data.mongoDescriptor,
this.cloudlyRef.config.data.mongoDescriptor!,
);
await this.smartdataDb.init();
}
+6 -6
View File
@@ -3,12 +3,12 @@ import * as paths from './paths.js';
export const logger = new plugins.smartlog.Smartlog({
logContext: {
company: null,
environment: null,
runtime: null,
zone: null,
companyunit: null,
containerName: null,
company: undefined,
environment: undefined,
runtime: undefined,
zone: undefined,
companyunit: undefined,
containerName: undefined,
},
});
logger.enableConsole({
+2 -2
View File
@@ -20,7 +20,7 @@ export class CloudlyAuthManager {
public CAuthorization = plugins.smartdata.setDefaultManagerForDoc(this, Authorization);
public typedrouter = new plugins.typedrequest.TypedRouter();
public smartjwtInstance: plugins.smartjwt.SmartJwt<IJwtData>;
public smartjwtInstance!: plugins.smartjwt.SmartJwt<IJwtData>;
constructor(cloudlyRef: Cloudly) {
this.cloudlyRef = cloudlyRef;
@@ -73,7 +73,7 @@ export class CloudlyAuthManager {
identity: {
jwt,
userId: user.id,
name: user.data.username,
name: user.data.username || user.id,
expiresAt: expiresAtTimestamp,
role: user.data.role,
type: user.data.type,
+2 -2
View File
@@ -45,8 +45,8 @@ export class User extends plugins.smartdata.SmartDataDbDoc<
// INSTANCE
@plugins.smartdata.unI()
public id: string;
public id!: string;
@plugins.smartdata.svDb()
public data: plugins.servezoneInterfaces.data.IUser['data'];
public data!: plugins.servezoneInterfaces.data.IUser['data'];
}
+4 -2
View File
@@ -256,17 +256,19 @@ export class CloudlyBackupManager {
backup.tags = requestArg.tags;
await backup.save();
const replicationEnabled = (requestArg as any).replicate !== false && !!process.env.CLOUDLY_BACKUP_TARGET_TYPE;
try {
const result = await this.fireCoreflowRequest('executeServiceBackup', {
backupId: backup.id,
service: await service.createSavableObject(),
tags: requestArg.tags,
replication: {
enabled: true,
enabled: replicationEnabled,
},
}, backup.clusterId);
backup.snapshots = result.snapshots || [];
if (!result.replication) {
if (replicationEnabled && !result.replication) {
throw new Error('Coreflow did not complete remote backup replication');
}
backup.replication = result.replication;
@@ -67,7 +67,10 @@ class S3BackupTargetWriter implements IBackupTargetWriter {
: {}),
} as any);
const bucketName = requiredEnv('CLOUDLY_BACKUP_S3_BUCKET');
return await smartBucket.getBucketByName(bucketName) || await smartBucket.createBucket(bucketName);
if (await smartBucket.bucketExists(bucketName)) {
return await smartBucket.getBucketByName(bucketName);
}
return await smartBucket.createBucket(bucketName);
})();
}
return await this.bucketPromise;
+21 -12
View File
@@ -12,29 +12,38 @@ export class BareMetal extends plugins.smartdata.SmartDataDbDoc<
public static async createFromHetznerServer(
hetznerServerArg: plugins.hetznercloud.HetznerServer,
) {
const serverData = hetznerServerArg.data;
if (!serverData) {
throw new Error('Hetzner server response is missing server data');
}
const ipv4 = serverData.public_net.ipv4;
if (!ipv4) {
throw new Error(`Hetzner server ${serverData.id} has no primary IPv4 address`);
}
const newBareMetal = new BareMetal();
newBareMetal.id = plugins.smartunique.shortId(8);
const data: plugins.servezoneInterfaces.data.IBareMetal['data'] = {
hostname: hetznerServerArg.data.name,
primaryIp: hetznerServerArg.data.public_net.ipv4.ip,
hostname: serverData.name,
primaryIp: ipv4.ip,
provider: 'hetzner',
location: hetznerServerArg.data.datacenter.name,
location: serverData.datacenter.name,
specs: {
cpuModel: hetznerServerArg.data.server_type.cpu_type,
cpuCores: hetznerServerArg.data.server_type.cores,
memoryGB: hetznerServerArg.data.server_type.memory,
storageGB: hetznerServerArg.data.server_type.disk,
cpuModel: serverData.server_type.cpu_type,
cpuCores: serverData.server_type.cores,
memoryGB: serverData.server_type.memory,
storageGB: serverData.server_type.disk,
storageType: 'nvme',
},
powerState: hetznerServerArg.data.status === 'running' ? 'on' : 'off',
powerState: serverData.status === 'running' ? 'on' : 'off',
osInfo: {
name: 'Debian',
version: '12',
},
assignedNodeIds: [],
providerMetadata: {
hetznerServerId: hetznerServerArg.data.id,
hetznerServerName: hetznerServerArg.data.name,
hetznerServerId: serverData.id,
hetznerServerName: serverData.name,
},
};
Object.assign(newBareMetal, { data });
@@ -44,10 +53,10 @@ export class BareMetal extends plugins.smartdata.SmartDataDbDoc<
// INSTANCE
@plugins.smartdata.unI()
public id: string;
public id!: string;
@plugins.smartdata.svDb()
public data: plugins.servezoneInterfaces.data.IBareMetal['data'];
public data!: plugins.servezoneInterfaces.data.IBareMetal['data'];
constructor() {
super();
@@ -7,7 +7,7 @@ export class CloudlyBaremetalManager {
public cloudlyRef: Cloudly;
public typedRouter = new plugins.typedrequest.TypedRouter();
public hetznerAccount: plugins.hetznercloud.HetznerAccount;
public hetznerAccount?: plugins.hetznercloud.HetznerAccount;
public get db() {
return this.cloudlyRef.mongodbConnector.smartdataDb;
@@ -119,18 +119,22 @@ export class CloudlyBaremetalManager {
* Create baremetal from Hetzner server
*/
public async createBaremetalFromHetznerServer(hetznerServer: plugins.hetznercloud.HetznerServer): Promise<BareMetal> {
const serverData = hetznerServer.data;
if (!serverData) {
throw new Error('Hetzner server response is missing server data');
}
// Check if baremetal already exists for this Hetzner server
const existingBaremetals = await this.CBareMetal.getInstances({});
for (const baremetal of existingBaremetals) {
if (baremetal.data.providerMetadata?.hetznerServerId === hetznerServer.data.id) {
logger.log('info', `BareMetal already exists for Hetzner server ${hetznerServer.data.id}`);
if (baremetal.data.providerMetadata?.hetznerServerId === serverData.id) {
logger.log('info', `BareMetal already exists for Hetzner server ${serverData.id}`);
return baremetal;
}
}
// Create new baremetal
const newBaremetal = await BareMetal.createFromHetznerServer(hetznerServer);
logger.log('success', `Created new BareMetal ${newBaremetal.id} from Hetzner server ${hetznerServer.data.id}`);
logger.log('success', `Created new BareMetal ${newBaremetal.id} from Hetzner server ${serverData.id}`);
return newBaremetal;
}
+36 -42
View File
@@ -305,15 +305,14 @@ export class CloudlyBaseOsManager {
}
public async handleRegisterHttpRequest(
reqArg: plugins.typedserver.Request,
resArg: plugins.typedserver.Response,
) {
ctxArg: plugins.typedserver.IRequestContext,
): Promise<Response> {
try {
const requestData = await this.readJsonBody<IBaseOsRegisterRequest>(reqArg);
const requestData = await this.readJsonBody<IBaseOsRegisterRequest>(ctxArg);
const response = await this.registerNode(requestData);
this.sendJson(resArg, 200, response);
return this.createJsonResponse(200, response);
} catch (error) {
this.sendJson(resArg, 400, {
return this.createJsonResponse(400, {
accepted: false,
message: `BaseOS registration failed: ${(error as Error).message}`,
} satisfies IBaseOsRegisterResponse);
@@ -321,15 +320,14 @@ export class CloudlyBaseOsManager {
}
public async handleHeartbeatHttpRequest(
reqArg: plugins.typedserver.Request,
resArg: plugins.typedserver.Response,
) {
ctxArg: plugins.typedserver.IRequestContext,
): Promise<Response> {
try {
const requestData = await this.readJsonBody<IBaseOsHeartbeatRequest>(reqArg);
const requestData = await this.readJsonBody<IBaseOsHeartbeatRequest>(ctxArg);
const response = await this.acceptHeartbeat(requestData);
this.sendJson(resArg, 200, response);
return this.createJsonResponse(200, response);
} catch (error) {
this.sendJson(resArg, 400, {
return this.createJsonResponse(400, {
accepted: false,
message: `BaseOS heartbeat failed: ${(error as Error).message}`,
} satisfies IBaseOsHeartbeatResponse);
@@ -337,37 +335,35 @@ export class CloudlyBaseOsManager {
}
public async handleImageDownloadHttpRequest(
reqArg: plugins.typedserver.Request,
resArg: plugins.typedserver.Response,
) {
ctxArg: plugins.typedserver.IRequestContext,
): Promise<Response> {
try {
const requestUrl = new URL((reqArg as any).originalUrl || reqArg.url || '/', 'http://localhost');
const buildId = requestUrl.pathname.split('/').at(-2);
const token = requestUrl.searchParams.get('token');
const buildId = ctxArg.params.buildId || ctxArg.url.pathname.split('/').at(-2);
const token = ctxArg.url.searchParams.get('token');
if (!buildId || !token) {
this.sendJson(resArg, 400, { errorText: 'build id or download token missing' });
return;
return this.createJsonResponse(400, { errorText: 'build id or download token missing' });
}
const build = await this.getImageBuildById(buildId);
if (build.downloadTokenHash !== this.hashSecret(token) || (build.downloadTokenExpiresAt || 0) < Date.now()) {
this.sendJson(resArg, 403, { errorText: 'download token is invalid or expired' });
return;
return this.createJsonResponse(403, { errorText: 'download token is invalid or expired' });
}
if (build.data.status !== 'ready' || !build.data.artifact) {
this.sendJson(resArg, 409, { errorText: 'image build is not ready' });
return;
return this.createJsonResponse(409, { errorText: 'image build is not ready' });
}
const artifact = build.data.artifact;
const bucket = await this.getArtifactBucket(artifact.bucketName);
const artifactStream = await bucket.fastGetStream({ path: artifact.key }, 'nodestream');
resArg.status(200);
resArg.setHeader('Content-Type', artifact.contentType || 'application/octet-stream');
resArg.setHeader('Content-Length', String(artifact.size));
resArg.setHeader('Content-Disposition', `attachment; filename="${artifact.filename}"`);
(artifactStream as nodeStream.Readable).pipe(resArg as any);
return new Response(nodeStream.Readable.toWeb(artifactStream as nodeStream.Readable) as ReadableStream, {
status: 200,
headers: {
'Content-Type': artifact.contentType || 'application/octet-stream',
'Content-Length': String(artifact.size),
'Content-Disposition': `attachment; filename="${artifact.filename}"`,
},
});
} catch (error) {
this.sendJson(resArg, 500, {
return this.createJsonResponse(500, {
errorText: `BaseOS image download failed: ${(error as Error).message}`,
});
}
@@ -986,22 +982,20 @@ export class CloudlyBaseOsManager {
&& typeof runtimeInfo.checkedAt === 'number';
}
private async readJsonBody<T>(reqArg: plugins.typedserver.Request): Promise<T> {
const chunks: Buffer[] = [];
for await (const chunk of reqArg as any) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
const bodyString = Buffer.concat(chunks).toString('utf8').trim();
private async readJsonBody<T>(ctxArg: plugins.typedserver.IRequestContext): Promise<T> {
const bodyString = (await ctxArg.text()).trim();
return bodyString ? JSON.parse(bodyString) as T : {} as T;
}
private sendJson(
resArg: plugins.typedserver.Response,
private createJsonResponse(
statusCodeArg: number,
bodyArg: object,
) {
resArg.status(statusCodeArg);
resArg.setHeader('Content-Type', 'application/json');
resArg.end(JSON.stringify(bodyArg));
): Response {
return new Response(JSON.stringify(bodyArg), {
status: statusCodeArg,
headers: {
'Content-Type': 'application/json',
},
});
}
}
+2 -2
View File
@@ -17,10 +17,10 @@ export class Cluster extends plugins.smartdata.SmartDataDbDoc<
// INSTANCE
@plugins.smartdata.unI()
public id: string;
public id!: string;
@plugins.smartdata.svDb()
public data: plugins.servezoneInterfaces.data.ICluster['data'];
public data!: plugins.servezoneInterfaces.data.ICluster['data'];
constructor() {
super();
+41 -8
View File
@@ -22,16 +22,19 @@ export class ClusterManager {
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.cluster.IRequest_CreateCluster>(
new plugins.typedrequest.TypedHandler('createCluster', async (dataArg) => {
// TODO: guards
new plugins.typedrequest.TypedHandler('createCluster', async (dataArg, toolsArg) => {
await toolsArg!.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], dataArg);
const setupMode = dataArg.setupMode || 'manual'; // Default to manual if not specified
const cluster = await this.createCluster({
id: plugins.smartunique.uniSimple('cluster'),
data: {
userId: null, // this is created by the createCluster method
userId: '', // this is created by the createCluster method
name: dataArg.clusterName,
setupMode: setupMode,
acmeInfo: null,
acmeInfo: {
serverAddress: '',
serverSecret: '',
},
cloudlyUrl: `https://${this.cloudlyRef.config.data.publicUrl}:${this.cloudlyRef.config.data.publicPort}/`,
nodes: [],
sshKeys: [],
@@ -51,8 +54,8 @@ export class ClusterManager {
);
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.cluster.IReq_Any_Cloudly_GetClusters>(
new plugins.typedrequest.TypedHandler('getClusters', async (dataArg) => {
// TODO: do authentication here
new plugins.typedrequest.TypedHandler('getClusters', async (dataArg, toolsArg) => {
await toolsArg!.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], dataArg);
const clusters = await this.getAllClusters();
return {
clusters: await Promise.all(
@@ -62,10 +65,41 @@ export class ClusterManager {
}),
);
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.cluster.IReq_Any_Cloudly_GetClusterById>(
new plugins.typedrequest.TypedHandler('getClusterById', async (dataArg, toolsArg) => {
await toolsArg!.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], dataArg);
const cluster = await this.CCluster.getInstance({ id: (dataArg as any).clusterId });
if (!cluster) {
throw new plugins.typedrequest.TypedResponseError('Cluster not found');
}
return {
cluster: await cluster.createSavableObject(),
};
}),
);
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.cluster.IReq_Any_Cloudly_UpdateCluster>(
new plugins.typedrequest.TypedHandler('updateCluster', async (dataArg, toolsArg) => {
await toolsArg!.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], dataArg);
const cluster = await this.CCluster.getInstance({ id: (dataArg as any).clusterId });
if (!cluster) {
throw new plugins.typedrequest.TypedResponseError('Cluster not found');
}
cluster.data = {
...cluster.data,
...dataArg.clusterData,
};
await cluster.save();
return {
resultCluster: await cluster.createSavableObject(),
};
}),
);
// delete cluster
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.cluster.IReq_Any_Cloudly_DeleteClusterById>(
new plugins.typedrequest.TypedHandler('deleteClusterById', async (reqDataArg, toolsArg) => {
await toolsArg.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], reqDataArg);
await toolsArg!.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], reqDataArg);
await this.deleteCluster(reqDataArg.clusterId);
return {
ok: true,
@@ -134,7 +168,6 @@ export class ClusterManager {
* @param configObjectArg
*/
public async createCluster(configObjectArg: plugins.servezoneInterfaces.data.ICluster) {
// TODO: guards
// lets create the cluster user
const clusterUser = new this.cloudlyRef.authManager.CUser({
id: await this.cloudlyRef.authManager.CUser.getNewId(),
+6 -6
View File
@@ -9,28 +9,28 @@ export class Deployment extends plugins.smartdata.SmartDataDbDoc<
public id: string = plugins.smartunique.uniSimple('deployment');
@plugins.smartdata.svDb()
public serviceId: string;
public serviceId!: string;
@plugins.smartdata.svDb()
public nodeId: string;
public nodeId!: string;
@plugins.smartdata.svDb()
public containerId?: string;
@plugins.smartdata.svDb()
public usedImageId: string;
public usedImageId!: string;
@plugins.smartdata.svDb()
public version: string;
public version!: string;
@plugins.smartdata.svDb()
public deployedAt: number;
public deployedAt!: number;
@plugins.smartdata.svDb()
public deploymentLog: string[] = [];
@plugins.smartdata.svDb()
public status: 'scheduled' | 'starting' | 'running' | 'stopping' | 'stopped' | 'failed';
public status!: 'scheduled' | 'starting' | 'running' | 'stopping' | 'stopped' | 'failed';
@plugins.smartdata.svDb()
public healthStatus?: 'healthy' | 'unhealthy' | 'unknown';
+2 -2
View File
@@ -79,10 +79,10 @@ export class DnsEntry extends plugins.smartdata.SmartDataDbDoc<
// INSTANCE
@plugins.smartdata.svDb()
public id: string;
public id!: string;
@plugins.smartdata.svDb()
public data: plugins.servezoneInterfaces.data.IDnsEntry['data'];
public data!: plugins.servezoneInterfaces.data.IDnsEntry['data'];
/**
* Validates the DNS entry data
+2 -2
View File
@@ -95,10 +95,10 @@ export class Domain extends plugins.smartdata.SmartDataDbDoc<
// INSTANCE
@plugins.smartdata.unI()
public id: string;
public id!: string;
@plugins.smartdata.svDb()
public data: plugins.servezoneInterfaces.data.IDomain['data'];
public data!: plugins.servezoneInterfaces.data.IDomain['data'];
/**
* Verify domain ownership
@@ -107,10 +107,10 @@ export class ExternalRegistry extends plugins.smartdata.SmartDataDbDoc<ExternalR
// INSTANCE
@plugins.smartdata.svDb()
public id: string;
public id!: string;
@plugins.smartdata.svDb()
public data: plugins.servezoneInterfaces.data.IExternalRegistry['data'];
public data!: plugins.servezoneInterfaces.data.IExternalRegistry['data'];
constructor() {
super();
@@ -170,10 +170,11 @@ export class ExternalRegistry extends plugins.smartdata.SmartDataDbDoc<ExternalR
return { success: false, message: 'Unknown registry type' };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.data.status = 'error';
this.data.lastError = error.message;
this.data.lastError = errorMessage;
await this.save();
return { success: false, message: error.message };
return { success: false, message: errorMessage };
}
}
+2 -2
View File
@@ -26,10 +26,10 @@ export class Image extends plugins.smartdata.SmartDataDbDoc<
}
@plugins.smartdata.unI()
public id: string;
public id!: string;
@plugins.smartdata.svDb()
public data: plugins.servezoneInterfaces.data.IImage['data'];
public data!: plugins.servezoneInterfaces.data.IImage['data'];
public async getVersions() {}
+45 -17
View File
@@ -7,9 +7,9 @@ import { Image } from './classes.image.js';
export class ImageManager {
cloudlyRef: Cloudly;
public typedrouter = new plugins.typedrequest.TypedRouter();
public smartbucketInstance: plugins.smartbucket.SmartBucket;
public imageDir: plugins.smartbucket.Directory;
public dockerImageStore: plugins.docker.DockerImageStore;
public smartbucketInstance!: plugins.smartbucket.SmartBucket;
public imageDir!: plugins.smartbucket.Directory;
public dockerImageStore!: plugins.docker.DockerImageStore;
get db() {
return this.cloudlyRef.mongodbConnector.smartdataDb;
@@ -26,7 +26,7 @@ export class ImageManager {
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_CreateImage>(
'createImage',
async (reqArg, toolsArg) => {
await toolsArg.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], reqArg);
await toolsArg!.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], reqArg);
const image = await this.CImage.create({
name: reqArg.name,
description: reqArg.description,
@@ -41,7 +41,7 @@ export class ImageManager {
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_GetImage>(
new plugins.typedrequest.TypedHandler('getImage', async (reqArg, toolsArg) => {
await toolsArg.passGuards([this.cloudlyRef.authManager.adminOrClusterIdentityGuard], reqArg);
await toolsArg!.passGuards([this.cloudlyRef.authManager.adminOrClusterIdentityGuard], reqArg);
const image = await this.CImage.getInstance({
id: reqArg.imageId,
});
@@ -55,7 +55,7 @@ export class ImageManager {
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_DeleteImage>(
'deleteImage',
async (reqArg, toolsArg) => {
await toolsArg.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], reqArg);
await toolsArg!.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], reqArg);
const image = await this.CImage.getInstance({
id: reqArg.imageId,
});
@@ -69,7 +69,7 @@ export class ImageManager {
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_GetAllImages>(
'getAllImages',
async (requestArg, toolsArg) => {
await toolsArg.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], requestArg);
await toolsArg!.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], requestArg);
const images = await this.CImage.getInstances({});
return {
images: await Promise.all(
@@ -96,9 +96,24 @@ export class ImageManager {
throw new plugins.typedrequest.TypedResponseError('Image not found');
}
const imageVersion = reqArg.versionString;
if (!imageVersion) {
throw new plugins.typedrequest.TypedResponseError('versionString is required');
}
console.log(
`got request to push image version ${imageVersion} for image ${refImage.data.name}`,
);
const storagePath = await refImage.getStoragePath(imageVersion);
refImage.data.versions = [
...refImage.data.versions.filter((version) => version.versionString !== imageVersion),
{
versionString: imageVersion,
source: 'upload',
storagePath,
size: 0,
createdAt: Date.now(),
},
];
await refImage.save();
const imagePushStream = reqArg.imageStream;
(async () => {
const smartWebDuplex = new plugins.smartstream.webstream.WebDuplexStream<
@@ -112,10 +127,12 @@ export class ImageManager {
});
imagePushStream.writeToWebstream(smartWebDuplex.writable);
await this.dockerImageStore.storeImage(
refImage.id,
storagePath,
plugins.smartstream.SmartDuplex.fromWebReadableStream(smartWebDuplex.readable),
);
})();
})().catch((error) => {
console.error(`failed to store image ${refImage.id}:${imageVersion}`, error);
});
return {
allowed: true,
};
@@ -133,13 +150,21 @@ export class ImageManager {
const imageVersion = image.data.versions.find(
(version) => version.versionString === reqArg.versionString,
);
const readable = this.imageDir.fastGetStream(
if (!imageVersion) {
throw new plugins.typedrequest.TypedResponseError('Image version not found');
}
const readable = await this.imageDir.fastGetStream(
{
path: await image.getStoragePath(reqArg.versionString),
path: imageVersion.storagePath || await image.getStoragePath(reqArg.versionString),
},
'webstream',
);
const imageVirtualStream = new plugins.typedrequest.VirtualStream();
(async () => {
await imageVirtualStream.readFromWebstream(readable);
})().catch((error) => {
console.error(`failed to stream image ${image.id}:${reqArg.versionString}`, error);
});
return {
imageStream: imageVirtualStream,
};
@@ -150,21 +175,24 @@ export class ImageManager {
public async start() {
// lets setup s3
const s3Descriptor: plugins.tsclass.storage.IS3Descriptor =
await this.cloudlyRef.config.appData.waitForAndGetKey('s3Descriptor');
const s3Descriptor =
await this.cloudlyRef.config.appData.waitForAndGetKey('s3Descriptor') as plugins.tsclass.storage.IS3Descriptor;
console.log(this.cloudlyRef.config.data.s3Descriptor);
this.smartbucketInstance = new plugins.smartbucket.SmartBucket(
this.cloudlyRef.config.data.s3Descriptor,
this.cloudlyRef.config.data.s3Descriptor!,
);
const bucket = await this.smartbucketInstance.getBucketByName(s3Descriptor.bucketName);
await bucket.fastPut({ path: 'images/00init', contents: 'init' });
const bucketName = s3Descriptor.bucketName!;
const bucket = await this.smartbucketInstance.bucketExists(bucketName)
? await this.smartbucketInstance.getBucketByName(bucketName)
: await this.smartbucketInstance.createBucket(bucketName);
await bucket.fastPut({ path: 'images/00init', contents: 'init', overwrite: true });
this.imageDir = await bucket.getDirectoryFromPath({
path: '/images',
});
// lets setup dockerstore
await plugins.smartfile.fs.ensureDir(paths.dockerImageStoreDir);
await plugins.fsPromises.mkdir(paths.dockerImageStoreDir, { recursive: true });
this.dockerImageStore = new plugins.docker.DockerImageStore({
localDirPath: paths.dockerImageStoreDir,
bucketDir: this.imageDir,
+2 -2
View File
@@ -34,10 +34,10 @@ export class ClusterNode extends plugins.smartdata.SmartDataDbDoc<
// INSTANCE
@plugins.smartdata.unI()
public id: string;
public id!: string;
@plugins.smartdata.svDb()
public data: plugins.servezoneInterfaces.data.IClusterNode['data'];
public data!: plugins.servezoneInterfaces.data.IClusterNode['data'];
constructor() {
super();
+25 -14
View File
@@ -1,6 +1,7 @@
import { logger } from '../logger.js';
import * as plugins from '../plugins.js';
import type { CloudlyNodeManager } from './classes.nodemanager.js';
import type { Cluster } from '../manager.cluster/classes.cluster.js';
export class CurlFresh {
public optionsArg = {
@@ -39,33 +40,33 @@ bash -c "npm config set registry ${this.optionsArg.npmRegistry}"
bash -c "pnpm install -g @serve.zone/spark"
# lets install the spark daemon
bash -c "spark installdaemon"
# TODO: start spark with jump code
bash -c "spark installdaemon --mode=coreflow-node --cloudlyUrl='__CLOUDLY_URL__' --jumpcode='__JUMPCODE__'"
`,
};
public nodeManagerRef: CloudlyNodeManager;
public curlFreshRoute: plugins.typedserver.servertools.Route;
public handler = new plugins.typedserver.servertools.Handler('ALL', async (req, res) => {
public async handleRequest(ctx: plugins.typedserver.IRequestContext): Promise<Response> {
logger.log('info', 'curlfresh handler called. a server might be coming online soon :)');
const scriptname = req.params.scriptname;
const scriptname = ctx.params.scriptname;
switch (scriptname) {
case 'setup.sh':
logger.log('info', 'sending setup.sh');
res.type('application/x-sh');
res.send(this.scripts['setup.sh']);
break;
return new Response(this.scripts['setup.sh']
.replaceAll('__CLOUDLY_URL__', ctx.url.searchParams.get('cloudlyUrl') || '')
.replaceAll('__JUMPCODE__', ctx.url.searchParams.get('jumpcode') || ''), {
headers: {
'Content-Type': 'application/x-sh',
},
});
default:
res.send('no script found');
break;
return new Response('no script found', { status: 404 });
}
});
}
constructor(nodeManagerRefArg: CloudlyNodeManager) {
this.nodeManagerRef = nodeManagerRefArg;
}
public async getServerUserData(): Promise<string> {
public async getServerUserData(clusterArg?: Cluster): Promise<string> {
const sslMode =
await this.nodeManagerRef.cloudlyRef.config.appData.waitForAndGetKey('sslMode');
let protocol: 'http' | 'https';
@@ -80,9 +81,19 @@ bash -c "spark installdaemon"
const port =
await this.nodeManagerRef.cloudlyRef.config.appData.waitForAndGetKey('publicPort');
let cloudlyUrl = `${protocol}://${domain}:${port}/`;
let jumpcode = '';
if (clusterArg?.data.userId) {
const clusterUser = await this.nodeManagerRef.cloudlyRef.authManager.CUser.getInstance({
id: clusterArg.data.userId,
});
jumpcode = clusterUser?.data.tokens?.[0]?.token || '';
cloudlyUrl = clusterArg.data.cloudlyUrl || cloudlyUrl;
}
const serverUserData = `#cloud-config
runcmd:
- curl -o- ${protocol}://${domain}:${port}/curlfresh/setup.sh | sh
- curl -o- '${protocol}://${domain}:${port}/curlfresh/setup.sh?cloudlyUrl=${encodeURIComponent(cloudlyUrl)}&jumpcode=${encodeURIComponent(jumpcode)}' | sh
`;
console.log(serverUserData);
return serverUserData;
+10 -5
View File
@@ -9,7 +9,7 @@ export class CloudlyNodeManager {
public typedRouter = new plugins.typedrequest.TypedRouter();
public curlfreshInstance = new CurlFresh(this);
public hetznerAccount: plugins.hetznercloud.HetznerAccount;
public hetznerAccount?: plugins.hetznercloud.HetznerAccount;
public get db() {
return this.cloudlyRef.mongodbConnector.smartdataDb;
@@ -65,13 +65,17 @@ export class CloudlyNodeManager {
console.log(`Skipping node provisioning for cluster ${cluster.id} - setupMode is ${cluster.data.setupMode || 'manual'}`);
continue;
}
const hetznerAccount = this.hetznerAccount;
if (!hetznerAccount) {
throw new Error('Hetzner account is not configured');
}
// get existing nodes
const nodes = await this.getNodesByCluster(cluster);
// if there is no node, create one
if (nodes.length === 0) {
const hetznerServer = await this.hetznerAccount.createServer({
const hetznerServer = await hetznerAccount.createServer({
name: plugins.smartunique.uniSimple('node'),
location: 'nbg1',
type: 'cpx41',
@@ -79,7 +83,7 @@ export class CloudlyNodeManager {
clusterId: cluster.id,
priority: '1',
},
userData: await this.curlfreshInstance.getServerUserData(),
userData: await this.curlfreshInstance.getServerUserData(cluster),
});
// First create BareMetal record
@@ -94,12 +98,12 @@ export class CloudlyNodeManager {
);
// if there is a node, make sure that it exists
for (const node of nodes) {
const hetznerServers = await this.hetznerAccount.getServersByLabel({
const hetznerServers = await hetznerAccount.getServersByLabel({
clusterId: cluster.id,
});
if (!hetznerServers || hetznerServers.length === 0) {
console.log(`node ${node.id} does not exist in the real world. Creating it now...`);
const hetznerServer = await this.hetznerAccount.createServer({
const hetznerServer = await hetznerAccount.createServer({
name: plugins.smartunique.uniSimple('node'),
location: 'nbg1',
type: 'cpx41',
@@ -107,6 +111,7 @@ export class CloudlyNodeManager {
clusterId: cluster.id,
priority: '1',
},
userData: await this.curlfreshInstance.getServerUserData(cluster),
});
// First create BareMetal record
@@ -10,11 +10,11 @@ export class PlatformBinding extends plugins.smartdata.SmartDataDbDoc<
public static async upsertBinding(
bindingArg: plugins.servezoneInterfaces.platform.IPlatformBinding,
) {
const existingBinding =
bindingArg.id &&
(await this.getInstance({
const existingBinding = bindingArg.id
? await this.getInstance({
id: bindingArg.id,
}));
})
: undefined;
const binding = existingBinding || new PlatformBinding();
const timestamp = Date.now();
+60 -61
View File
@@ -66,37 +66,32 @@ export class CloudlyRegistryManager {
}
public async handleHttpRequest(
req: plugins.typedserver.Request,
res: plugins.typedserver.Response,
) {
ctx: plugins.typedserver.IRequestContext,
): Promise<Response> {
try {
const requestUrl = new URL((req as any).originalUrl || req.url || '/', 'http://localhost');
const requestUrl = ctx.url;
if (requestUrl.pathname === '/v2/token') {
await this.handleTokenRequest(req, res, requestUrl);
return;
return await this.handleTokenRequest(ctx, requestUrl);
}
if (!this.started) {
res.status(503);
res.end('registry is not ready');
return;
return new Response('registry is not ready', { status: 503 });
}
const rawBody = await this.getRawBody(req);
const rawBody = Buffer.from(await ctx.request.arrayBuffer());
const response = await this.smartRegistry.handleRequest({
method: req.method || 'GET',
method: ctx.method || 'GET',
path: requestUrl.pathname,
query: Object.fromEntries(requestUrl.searchParams),
headers: this.headersToRecord(req.headers),
headers: this.headersToRecord(ctx.headers),
rawBody: rawBody.length > 0 ? rawBody : undefined,
});
await this.sendRegistryResponse(res, response);
return this.createRegistryResponse(response);
} catch (error) {
logger.log('error', `registry request failed: ${(error as Error).message}`);
res.status(500);
res.end('registry request failed');
return new Response('registry request failed', { status: 500 });
}
}
@@ -259,16 +254,17 @@ export class CloudlyRegistryManager {
}
private async handleTokenRequest(
req: plugins.typedserver.Request,
res: plugins.typedserver.Response,
ctx: plugins.typedserver.IRequestContext,
requestUrl: URL,
) {
const user = await this.authenticateRequest(req);
): Promise<Response> {
const user = await this.authenticateRequest(ctx);
if (!user) {
res.status(401);
res.setHeader('WWW-Authenticate', 'Basic realm="Cloudly Registry"');
res.end('authentication required');
return;
return new Response('authentication required', {
status: 401,
headers: {
'WWW-Authenticate': 'Basic realm="Cloudly Registry"',
},
});
}
const requestedScopes = this.getRequestedOciScopes(requestUrl.searchParams);
@@ -277,9 +273,7 @@ export class CloudlyRegistryManager {
return action === 'push' || action === 'delete';
});
if (requestedWriteAccess && !user.canWrite) {
res.status(403);
res.end('registry write access denied');
return;
return new Response('registry write access denied', { status: 403 });
}
const token = await this.smartRegistry.getAuthManager().createOciToken(
@@ -287,22 +281,26 @@ export class CloudlyRegistryManager {
requestedScopes,
3600,
);
res.status(200);
res.setHeader('Content-Type', 'application/json');
res.end(
return new Response(
JSON.stringify({
token,
access_token: token,
expires_in: 3600,
issued_at: new Date().toISOString(),
}),
{
status: 200,
headers: {
'Content-Type': 'application/json',
},
},
);
}
private async authenticateRequest(
req: plugins.typedserver.Request,
ctx: plugins.typedserver.IRequestContext,
): Promise<TAuthenticatedRegistryUser | null> {
const credentials = this.getBasicCredentials(req);
const credentials = this.getBasicCredentials(ctx);
if (!credentials) {
return null;
}
@@ -332,8 +330,8 @@ export class CloudlyRegistryManager {
return null;
}
private getBasicCredentials(req: plugins.typedserver.Request) {
const authHeader = req.headers.authorization;
private getBasicCredentials(ctx: plugins.typedserver.IRequestContext) {
const authHeader = ctx.headers.get('authorization');
if (!authHeader?.startsWith('Basic ')) {
return null;
}
@@ -370,48 +368,49 @@ export class CloudlyRegistryManager {
return `${this.cloudlyRef.config.data.sslMode === 'none' ? 'http' : 'https'}://${this.getRegistryHost()}`;
}
private headersToRecord(headersArg: plugins.typedserver.Request['headers']) {
return Object.fromEntries(
Object.entries(headersArg).map(([key, value]) => [
key.toLowerCase(),
Array.isArray(value) ? value.join(', ') : value || '',
]),
);
private headersToRecord(headersArg: Headers) {
const headers: Record<string, string> = {};
headersArg.forEach((value, key) => {
headers[key.toLowerCase()] = value;
});
return headers;
}
private async getRawBody(req: plugins.typedserver.Request) {
const chunks: Buffer[] = [];
for await (const chunk of req as any) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
return Buffer.concat(chunks);
}
private async sendRegistryResponse(
res: plugins.typedserver.Response,
private createRegistryResponse(
responseArg: plugins.smartregistry.IResponse,
) {
res.status(responseArg.status);
): Response {
const headers = new Headers();
for (const [key, value] of Object.entries(responseArg.headers)) {
res.setHeader(key, value);
headers.set(key, value);
}
if (!responseArg.body) {
res.end();
return;
return new Response(null, {
status: responseArg.status,
headers,
});
}
if (responseArg.body instanceof ReadableStream) {
plugins.stream.Readable.fromWeb(responseArg.body as any).pipe(res);
return;
return new Response(responseArg.body, {
status: responseArg.status,
headers,
});
}
if (Buffer.isBuffer(responseArg.body) || typeof responseArg.body === 'string') {
res.end(responseArg.body);
return;
return new Response(responseArg.body as BodyInit, {
status: responseArg.status,
headers,
});
}
res.setHeader('Content-Type', responseArg.headers['Content-Type'] || 'application/json');
res.end(JSON.stringify(responseArg.body));
if (!headers.has('Content-Type')) {
headers.set('Content-Type', 'application/json');
}
return new Response(JSON.stringify(responseArg.body), {
status: responseArg.status,
headers,
});
}
}
+2 -2
View File
@@ -12,10 +12,10 @@ export class SecretBundle extends plugins.smartdata.SmartDataDbDoc<
// INSTANCE
@plugins.smartdata.unI()
public id: string;
public id!: string;
@plugins.smartdata.svDb()
public data: plugins.servezoneInterfaces.data.ISecretBundle['data'];
public data!: plugins.servezoneInterfaces.data.ISecretBundle['data'];
public async getSecretGroups() {
const secretGroups: SecretGroup[] = [];
+2 -2
View File
@@ -14,8 +14,8 @@ export class SecretGroup extends plugins.smartdata.SmartDataDbDoc<
* the insatnce id. This should be a random id, except for default
*/
@plugins.smartdata.unI()
id: string;
id!: string;
@plugins.smartdata.svDb()
data: plugins.servezoneInterfaces.data.ISecretGroup['data'];
data!: plugins.servezoneInterfaces.data.ISecretGroup['data'];
}
+8 -5
View File
@@ -18,9 +18,9 @@ export class CloudlySecretManager {
// INSTANCE
public cloudlyRef: Cloudly;
public projectinfo = new plugins.projectinfo.ProjectinfoNpm(paths.packageDir);
public projectinfo = plugins.projectinfo.ProjectinfoNpm.create(paths.packageDir);
public serviceQenv = new plugins.qenv.Qenv(paths.packageDir, paths.nogitDir);
public typedrouter: plugins.typedrequest.TypedRouter;
public typedrouter!: plugins.typedrequest.TypedRouter;
get db() {
return this.cloudlyRef.mongodbConnector.smartdataDb;
@@ -40,7 +40,7 @@ export class CloudlySecretManager {
new plugins.typedrequest.TypedHandler(
'getSecretBundles',
async (dataArg, toolsArg) => {
await toolsArg.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], dataArg);
await toolsArg!.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], dataArg);
dataArg.identity.jwt;
const secretBundles = await SecretBundle.getInstances({});
return {
@@ -56,7 +56,7 @@ export class CloudlySecretManager {
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.secretbundle.IReq_GetSecretBundleById>(
new plugins.typedrequest.TypedHandler('getSecretBundleById', async (dataArg, toolsArg) => {
await toolsArg.passGuards([this.cloudlyRef.authManager.adminOrClusterIdentityGuard], dataArg);
await toolsArg!.passGuards([this.cloudlyRef.authManager.adminOrClusterIdentityGuard], dataArg);
const secretBundle = await SecretBundle.getInstance({
id: dataArg.secretBundleId,
});
@@ -108,7 +108,7 @@ export class CloudlySecretManager {
new plugins.typedrequest.TypedHandler(
'getSecretGroups',
async (dataArg, toolsArg) => {
await toolsArg.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], dataArg);
await toolsArg!.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], dataArg);
dataArg.identity.jwt;
const secretGroups = await SecretGroup.getInstances({});
return {
@@ -176,6 +176,9 @@ export class CloudlySecretManager {
const authorization = await wantedBundle.getAuthorizationFromAuthKey(
dataArg.secretBundleAuthorization.secretAccessKey,
);
if (!authorization) {
throw new plugins.typedrequest.TypedResponseError('secret bundle authorization not found');
}
return {
flatKeyValueObject: await wantedBundle.getKeyValueObjectForEnvironment(
authorization.environment,
+2 -2
View File
@@ -37,10 +37,10 @@ export class Service extends plugins.smartdata.SmartDataDbDoc<
// INSTANCE
@plugins.smartdata.unI()
public id: string;
public id!: string;
@plugins.smartdata.svDb()
public data: plugins.servezoneInterfaces.data.IService['data'];
public data!: plugins.servezoneInterfaces.data.IService['data'];
/**
* a service runs in a specific environment
@@ -94,6 +94,9 @@ export class ServiceManager {
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_CreateService>(
'createService',
async (dataArg) => {
await plugins.smartguard.passGuardsOrReject(dataArg, [
this.cloudlyRef.authManager.adminIdentityGuard,
]);
const service = await Service.createService(dataArg.serviceData);
service.data.registryTarget = this.cloudlyRef.registryManager.getServiceRegistryTarget(
service,
@@ -112,6 +115,9 @@ export class ServiceManager {
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_UpdateService>(
'updateService',
async (dataArg) => {
await plugins.smartguard.passGuardsOrReject(dataArg, [
this.cloudlyRef.authManager.adminIdentityGuard,
]);
const service = await Service.getInstance({
id: dataArg.serviceId,
});
@@ -136,6 +142,9 @@ export class ServiceManager {
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_DeleteServiceById>(
'deleteServiceById',
async (dataArg) => {
await plugins.smartguard.passGuardsOrReject(dataArg, [
this.cloudlyRef.authManager.adminIdentityGuard,
]);
const service = await Service.getInstance({
id: dataArg.serviceId,
});
@@ -5,7 +5,7 @@ import * as servezoneInterfaces from '@serve.zone/interfaces';
export class CloudlySettingsManager {
public cloudlyRef: Cloudly;
public readyDeferred = plugins.smartpromise.defer();
public settingsStore: plugins.smartdata.EasyStore<servezoneInterfaces.data.ICloudlySettings>;
public settingsStore!: plugins.smartdata.EasyStore<servezoneInterfaces.data.ICloudlySettings>;
constructor(cloudlyRefArg: Cloudly) {
this.cloudlyRef = cloudlyRefArg;
@@ -196,7 +196,7 @@ export class CloudlySettingsManager {
return { success: false, message: `Unknown provider: ${provider}` };
}
} catch (error) {
return { success: false, message: `Connection test failed: ${error.message}` };
return { success: false, message: `Connection test failed: ${error instanceof Error ? error.message : String(error)}` };
}
}
@@ -232,9 +232,10 @@ export class CloudlySettingsManager {
message: 'Settings updated successfully'
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
success: false,
message: `Failed to update settings: ${error.message}`
message: `Failed to update settings: ${errorMessage}`
};
}
}
@@ -254,9 +255,10 @@ export class CloudlySettingsManager {
message: `Setting ${requestData.key} cleared successfully`
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
success: false,
message: `Failed to clear setting: ${error.message}`
message: `Failed to clear setting: ${errorMessage}`
};
}
}
+2 -2
View File
@@ -64,10 +64,10 @@ export class TaskExecution extends plugins.smartdata.SmartDataDbDoc<
// INSTANCE
@plugins.smartdata.unI()
public id: string;
public id!: string;
@plugins.smartdata.svDb()
public data: plugins.servezoneInterfaces.data.ITaskExecution['data'];
public data!: plugins.servezoneInterfaces.data.ITaskExecution['data'];
/**
* Add a log entry to the execution
+5 -1
View File
@@ -75,7 +75,7 @@ export class CloudlyTaskManager {
taskName: string,
triggeredBy: 'schedule' | 'manual' | 'system',
userId?: string
): Promise<TaskExecution> {
): Promise<TaskExecution | null> {
const task = this.taskRegistry.get(taskName);
const info = this.taskInfo.get(taskName);
@@ -298,6 +298,9 @@ export class CloudlyTaskManager {
'manual',
reqArg.userId
);
if (!execution) {
throw new Error(`Task ${reqArg.taskName} did not start`);
}
return {
execution: await execution.createSavableObject(),
@@ -336,6 +339,7 @@ export class CloudlyTaskManager {
if (deletedCount > 0) {
logger.log('info', `Cleaned up ${deletedCount} old task executions`);
}
await this.taskBufferManager.start();
}
/**
+21 -13
View File
@@ -2,6 +2,8 @@ import * as plugins from '../plugins.js';
import { CloudlyTaskManager } from './classes.taskmanager.js';
import { logger } from '../logger.js';
const getErrorMessage = (errorArg: unknown) => errorArg instanceof Error ? errorArg.message : String(errorArg);
/**
* Create and register all predefined tasks
*/
@@ -74,7 +76,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
await execution?.addLog(`Cloudflare sync done: ${created} created, ${updated} updated`, 'success');
return { created, updated, totalZones: zones.length };
} catch (error) {
await execution?.addLog(`Cloudflare sync error: ${error.message}`, 'error');
await execution?.addLog(`Cloudflare sync error: ${getErrorMessage(error)}`, 'error');
throw error;
}
},
@@ -122,7 +124,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
await execution?.addLog(`Syncing DNS entry: ${entry.data.name}.${entry.data.zone}`, 'info');
syncedCount++;
} catch (error) {
await execution?.addLog(`Failed to sync ${entry.data.name}: ${error.message}`, 'warning');
await execution?.addLog(`Failed to sync ${entry.data.name}: ${getErrorMessage(error)}`, 'warning');
failedCount++;
}
}
@@ -133,7 +135,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
return { synced: syncedCount, failed: failedCount };
} catch (error) {
await execution?.addLog(`DNS sync error: ${error.message}`, 'error');
await execution?.addLog(`DNS sync error: ${getErrorMessage(error)}`, 'error');
throw error;
}
},
@@ -194,7 +196,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
return { renewed: renewedCount, upToDate: upToDateCount };
} catch (error) {
await execution?.addLog(`Certificate renewal error: ${error.message}`, 'error');
await execution?.addLog(`Certificate renewal error: ${getErrorMessage(error)}`, 'error');
throw error;
}
},
@@ -247,7 +249,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
images: deletedImages,
};
} catch (error) {
await execution?.addLog(`Cleanup error: ${error.message}`, 'error');
await execution?.addLog(`Cleanup error: ${getErrorMessage(error)}`, 'error');
throw error;
}
},
@@ -279,7 +281,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
let healthyCount = 0;
let unhealthyCount = 0;
const issues = [];
const issues: Array<{ deploymentId: string; serviceId: string; issue: string }> = [];
for (const deployment of deployments) {
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) {
@@ -316,7 +318,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
return { healthy: healthyCount, unhealthy: unhealthyCount, issues };
} catch (error) {
await execution?.addLog(`Health check error: ${error.message}`, 'error');
await execution?.addLog(`Health check error: ${getErrorMessage(error)}`, 'error');
throw error;
}
},
@@ -345,7 +347,13 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
// Get all nodes
const nodes = await taskManager.cloudlyRef.nodeManager.CClusterNode.getInstances({});
const report = {
const report: {
timestamp: number;
nodes: Array<{ nodeId: string; nodeName: string; cpu: number; memory: number; disk: number }>;
totalCpu: number;
totalMemory: number;
totalDisk: number;
} = {
timestamp: Date.now(),
nodes: [],
totalCpu: 0,
@@ -388,7 +396,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
return report;
} catch (error) {
await execution?.addLog(`Resource report error: ${error.message}`, 'error');
await execution?.addLog(`Resource report error: ${getErrorMessage(error)}`, 'error');
throw error;
}
},
@@ -428,7 +436,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
return { success: true };
} catch (error) {
await execution?.addLog(`Database maintenance error: ${error.message}`, 'error');
await execution?.addLog(`Database maintenance error: ${getErrorMessage(error)}`, 'error');
throw error;
}
},
@@ -454,7 +462,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
return;
}
const vulnerabilities = [];
const vulnerabilities: Array<{ type: string; severity: string; image: string; version: string }> = [];
// Check for exposed ports
await execution?.addLog('Checking for exposed ports...', 'info');
@@ -497,7 +505,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
return { vulnerabilities };
} catch (error) {
await execution?.addLog(`Security scan error: ${error.message}`, 'error');
await execution?.addLog(`Security scan error: ${getErrorMessage(error)}`, 'error');
throw error;
}
},
@@ -549,7 +557,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
networks: removedNetworks,
};
} catch (error) {
await execution?.addLog(`Docker cleanup error: ${error.message}`, 'error');
await execution?.addLog(`Docker cleanup error: ${getErrorMessage(error)}`, 'error');
throw error;
}
},
+4 -3
View File
@@ -2,8 +2,9 @@
import * as path from 'path';
import * as crypto from 'node:crypto';
import * as stream from 'stream';
import * as fsPromises from 'node:fs/promises';
export { path, crypto, stream };
export { path, crypto, stream, fsPromises };
// @apiglobal scope
import * as typedrequest from '@api.global/typedrequest';
@@ -25,7 +26,7 @@ import * as tsclass from '@tsclass/tsclass';
export { tsclass };
// @push.rocks scope
import * as npmextra from '@push.rocks/npmextra';
import * as smartconfig from '@push.rocks/smartconfig';
import * as projectinfo from '@push.rocks/projectinfo';
import * as qenv from '@push.rocks/qenv';
import * as smartacme from '@push.rocks/smartacme';
@@ -54,7 +55,7 @@ import * as taskbuffer from '@push.rocks/taskbuffer';
import * as typedserver from '@api.global/typedserver';
export {
npmextra,
smartconfig,
projectinfo,
qenv,
smartacme,
+2 -2
View File
@@ -23,7 +23,7 @@ It is not currently a full command tree for services, secrets, deployments, logs
The package is published from `cloudly/ts_cliclient` via `tspublish.json` under the name `@serve.zone/cli` with the `servezone` binary.
```sh
pnpm add -g @serve.zone/cli
pnpm add --global @serve.zone/cli
```
For local development inside the Cloudly repository, build the parent package:
@@ -56,7 +56,7 @@ When `CLOUDLY_TOKEN` is present, the CLI requests a stateful identity and asks C
## Programmatic Use
The submodule exports the `runCli()` entry point and uses `CliClient` internally:
The published submodule exports the `runCli()` entry point. For automation, most callers should use `@serve.zone/api` directly; the internal `CliClient` currently wraps `CloudlyApiClient` only to run the default cluster-list action:
```ts
import { CloudlyApiClient } from '@serve.zone/api';
+2 -1
View File
@@ -11,5 +11,6 @@
"registry.npmjs.org:public",
"verdaccio.lossless.digital:public"
],
"bin": ["servezone"]
"bin": ["servezone"],
"order": 10
}
+121 -117
View File
@@ -3,7 +3,7 @@ import * as domtools from '@design.estate/dees-domtools';
const appstate = new plugins.deesDomtools.plugins.smartstate.Smartstate();
export interface ILoginState {
identity: plugins.interfaces.data.IIdentity;
identity: plugins.interfaces.data.IIdentity | null;
}
export const loginStatePart: plugins.smartstate.StatePart<unknown, ILoginState> = await appstate.getStatePart<ILoginState>(
'login',
@@ -13,8 +13,8 @@ export const loginStatePart: plugins.smartstate.StatePart<unknown, ILoginState>
export const loginAction = loginStatePart.createAction<{ username: string; password: string }>(
async (statePartArg, payloadArg) => {
const currentState = statePartArg.getState();
let identity: plugins.interfaces.data.IIdentity = null;
const currentState = statePartArg.getState() || { identity: null };
let identity: plugins.interfaces.data.IIdentity | null = null;
try {
identity = await apiClient.loginWithUsernameAndPassword(payloadArg.username, payloadArg.password);
} catch (err) {
@@ -31,7 +31,7 @@ export const loginAction = loginStatePart.createAction<{ username: string; passw
if (!apiClient['typedsocketClient']) {
await apiClient.start();
}
try { apiClient.typedsocketClient.addTag('identity', apiClient.identity); } catch {}
try { await apiClient.typedsocketClient.setTag('identity', apiClient.identity); } catch {}
}
} catch {}
return newState;
@@ -39,7 +39,7 @@ export const loginAction = loginStatePart.createAction<{ username: string; passw
);
export const logoutAction = loginStatePart.createAction(async (statePartArg) => {
const currentState = statePartArg.getState();
const currentState = statePartArg.getState() || { identity: null };
return {
...currentState,
identity: null,
@@ -88,17 +88,21 @@ export const dataState = await appstate.getStatePart<IDataState>(
);
// Shared API client instance (used by UI actions)
type TCloudlyApiClientWithNullableIdentity = Omit<plugins.servezoneApi.CloudlyApiClient, 'identity'> & {
identity: plugins.interfaces.data.IIdentity | null;
};
export const apiClient = new plugins.servezoneApi.CloudlyApiClient({
registerAs: 'api',
cloudlyUrl: (typeof window !== 'undefined' && window.location?.origin) ? window.location.origin : undefined,
});
}) as TCloudlyApiClientWithNullableIdentity;
// Getting data
export const getAllDataAction = dataState.createAction(async (statePartArg) => {
let currentState = statePartArg.getState();
let currentState = statePartArg.getState() || {};
// SecretsGroups
try {
apiClient.identity = loginStatePart.getState().identity;
apiClient.identity = loginStatePart.getState()?.identity ?? null;
const secretGroups = await apiClient.secretgroup.getSecretGroups();
currentState = {
...currentState,
@@ -114,7 +118,7 @@ export const getAllDataAction = dataState.createAction(async (statePartArg) => {
// SecretBundles
try {
apiClient.identity = loginStatePart.getState().identity;
apiClient.identity = loginStatePart.getState()?.identity ?? null;
const responseSecretBundles = await apiClient.secretbundle.getSecretBundles();
currentState = {
...currentState,
@@ -130,7 +134,7 @@ export const getAllDataAction = dataState.createAction(async (statePartArg) => {
// images
try {
apiClient.identity = loginStatePart.getState().identity;
apiClient.identity = loginStatePart.getState()?.identity ?? null;
const images = await apiClient.image.getImages();
currentState = {
...currentState,
@@ -146,7 +150,7 @@ export const getAllDataAction = dataState.createAction(async (statePartArg) => {
// Clusters
try {
apiClient.identity = loginStatePart.getState().identity;
apiClient.identity = loginStatePart.getState()?.identity ?? null;
const clusters = await apiClient.cluster.getClusters();
currentState = {
...currentState,
@@ -162,7 +166,7 @@ export const getAllDataAction = dataState.createAction(async (statePartArg) => {
// External Registries via shared API client
try {
apiClient.identity = loginStatePart.getState().identity;
apiClient.identity = loginStatePart.getState()?.identity ?? null;
const registries = await apiClient.externalRegistry.getRegistries();
currentState = {
...currentState,
@@ -178,7 +182,7 @@ export const getAllDataAction = dataState.createAction(async (statePartArg) => {
// Services
try {
apiClient.identity = loginStatePart.getState().identity;
apiClient.identity = loginStatePart.getState()?.identity ?? null;
const services = await apiClient.services.getServices();
currentState = {
...currentState,
@@ -194,7 +198,7 @@ export const getAllDataAction = dataState.createAction(async (statePartArg) => {
// Deployments
try {
apiClient.identity = loginStatePart.getState().identity;
apiClient.identity = loginStatePart.getState()?.identity ?? null;
const responseDeployments = await apiClient.deployments.getDeployments();
currentState = {
...currentState,
@@ -210,7 +214,7 @@ export const getAllDataAction = dataState.createAction(async (statePartArg) => {
// Domains via API client
try {
apiClient.identity = loginStatePart.getState().identity;
apiClient.identity = loginStatePart.getState()?.identity ?? null;
const responseDomains = await apiClient.domains.getDomains();
currentState = {
...currentState,
@@ -226,7 +230,7 @@ export const getAllDataAction = dataState.createAction(async (statePartArg) => {
// DNS Entries via API client
try {
apiClient.identity = loginStatePart.getState().identity;
apiClient.identity = loginStatePart.getState()?.identity ?? null;
const responseDnsEntries = await apiClient.dns.getDnsEntries();
currentState = {
...currentState,
@@ -245,58 +249,57 @@ export const getAllDataAction = dataState.createAction(async (statePartArg) => {
// Service Actions
export const createServiceAction = dataState.createAction(
async (statePartArg, payloadArg: { serviceData: plugins.interfaces.data.IService['data'] }) => {
let currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
async (statePartArg, payloadArg: { serviceData: plugins.interfaces.data.IService['data'] }, context) => {
let currentState = statePartArg.getState() || {};
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.services.createService(payloadArg.serviceData);
currentState = await dataState.dispatchAction(getAllDataAction, null);
currentState = await context.dispatch(getAllDataAction, null);
return currentState;
}
);
export const updateServiceAction = dataState.createAction(
async (statePartArg, payloadArg: { serviceId: string; serviceData: plugins.interfaces.data.IService['data'] }) => {
let currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
async (statePartArg, payloadArg: { serviceId: string; serviceData: plugins.interfaces.data.IService['data'] }, context) => {
let currentState = statePartArg.getState() || {};
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.services.updateService(payloadArg.serviceId, payloadArg.serviceData);
currentState = await dataState.dispatchAction(getAllDataAction, null);
currentState = await context.dispatch(getAllDataAction, null);
return currentState;
}
);
export const deleteServiceAction = dataState.createAction(
async (statePartArg, payloadArg: { serviceId: string }) => {
let currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
async (statePartArg, payloadArg: { serviceId: string }, context) => {
let currentState = statePartArg.getState() || {};
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.services.deleteService(payloadArg.serviceId);
currentState = await dataState.dispatchAction(getAllDataAction, null);
currentState = await context.dispatch(getAllDataAction, null);
return currentState;
}
);
// SecretGroup Actions
export const createSecretGroupAction = dataState.createAction(
async (statePartArg, payloadArg: plugins.interfaces.data.ISecretGroup) => {
let currentState = statePartArg.getState();
async (statePartArg, payloadArg: { data: plugins.interfaces.data.ISecretGroup['data'] }, context) => {
let currentState = statePartArg.getState() || {};
try {
apiClient.identity = loginStatePart.getState().identity;
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.secretgroup.createSecretGroup(payloadArg.data);
currentState = await dataState.dispatchAction(getAllDataAction, null);
currentState = await context.dispatch(getAllDataAction, null);
} catch (err) {
console.error('Failed to create secret group:', err);
}
return currentState;
return currentState;
}
);
export const deleteSecretGroupAction = dataState.createAction(
async (statePartArg, payloadArg: { secretGroupId: string }) => {
let currentState = statePartArg.getState();
async (statePartArg, payloadArg: { secretGroupId: string }, context) => {
let currentState = statePartArg.getState() || {};
try {
apiClient.identity = loginStatePart.getState().identity;
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.secretgroup.deleteSecretGroupById(payloadArg.secretGroupId);
currentState = await dataState.dispatchAction(getAllDataAction, null);
currentState = await context.dispatch(getAllDataAction, null);
} catch (err) {
console.error('Failed to delete secret group:', err);
}
@@ -306,12 +309,12 @@ export const deleteSecretGroupAction = dataState.createAction(
// SecretBundle Actions
export const deleteSecretBundleAction = dataState.createAction(
async (statePartArg, payloadArg: { configBundleId: string }) => {
let currentState = statePartArg.getState();
async (statePartArg, payloadArg: { configBundleId: string }, context) => {
let currentState = statePartArg.getState() || {};
try {
apiClient.identity = loginStatePart.getState().identity;
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.secretbundle.deleteSecretBundleById(payloadArg.configBundleId);
currentState = await dataState.dispatchAction(getAllDataAction, null);
currentState = await context.dispatch(getAllDataAction, null);
} catch (err) {
console.error('Failed to delete secret bundle:', err);
}
@@ -321,146 +324,146 @@ export const deleteSecretBundleAction = dataState.createAction(
// image actions
export const createImageAction = dataState.createAction(
async (statePartArg, payloadArg: { imageName: string, description: string }) => {
let currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
async (statePartArg, payloadArg: { imageName: string, description: string }, context) => {
let currentState = statePartArg.getState() || {};
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.image.createImage({ name: payloadArg.imageName, description: payloadArg.description });
currentState = await dataState.dispatchAction(getAllDataAction, null);
currentState = await context.dispatch(getAllDataAction, null);
return currentState;
}
);
export const deleteImageAction = dataState.createAction(
async (statePartArg, payloadArg: { imageId: string }) => {
let currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
async (statePartArg, payloadArg: { imageId: string }, context) => {
let currentState = statePartArg.getState() || {};
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.image.deleteImage(payloadArg.imageId);
currentState = await dataState.dispatchAction(getAllDataAction, null);
currentState = await context.dispatch(getAllDataAction, null);
return currentState;
}
);
// Deployment Actions
export const createDeploymentAction = dataState.createAction(
async (statePartArg, payloadArg: { deploymentData: Partial<plugins.interfaces.data.IDeployment> }) => {
let currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
async (statePartArg, payloadArg: { deploymentData: Partial<plugins.interfaces.data.IDeployment> }, context) => {
let currentState = statePartArg.getState() || {};
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.deployments.createDeployment(payloadArg.deploymentData);
currentState = await dataState.dispatchAction(getAllDataAction, null);
currentState = await context.dispatch(getAllDataAction, null);
return currentState;
}
);
export const updateDeploymentAction = dataState.createAction(
async (statePartArg, payloadArg: { deploymentId: string; deploymentData: Partial<plugins.interfaces.data.IDeployment> }) => {
let currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
async (statePartArg, payloadArg: { deploymentId: string; deploymentData: Partial<plugins.interfaces.data.IDeployment> }, context) => {
let currentState = statePartArg.getState() || {};
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.deployments.updateDeployment(payloadArg.deploymentId, payloadArg.deploymentData);
currentState = await dataState.dispatchAction(getAllDataAction, null);
currentState = await context.dispatch(getAllDataAction, null);
return currentState;
}
);
export const deleteDeploymentAction = dataState.createAction(
async (statePartArg, payloadArg: { deploymentId: string }) => {
let currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
async (statePartArg, payloadArg: { deploymentId: string }, context) => {
let currentState = statePartArg.getState() || {};
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.deployments.deleteDeployment(payloadArg.deploymentId);
currentState = await dataState.dispatchAction(getAllDataAction, null);
currentState = await context.dispatch(getAllDataAction, null);
return currentState;
}
);
// DNS Actions
export const createDnsEntryAction = dataState.createAction(
async (statePartArg, payloadArg: { dnsEntryData: plugins.interfaces.data.IDnsEntry['data'] }) => {
let currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
async (statePartArg, payloadArg: { dnsEntryData: plugins.interfaces.data.IDnsEntry['data'] }, context) => {
let currentState = statePartArg.getState() || {};
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.dns.createDnsEntry(payloadArg.dnsEntryData);
currentState = await dataState.dispatchAction(getAllDataAction, null);
currentState = await context.dispatch(getAllDataAction, null);
return currentState;
}
);
export const updateDnsEntryAction = dataState.createAction(
async (statePartArg, payloadArg: { dnsEntryId: string; dnsEntryData: plugins.interfaces.data.IDnsEntry['data'] }) => {
let currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
async (statePartArg, payloadArg: { dnsEntryId: string; dnsEntryData: plugins.interfaces.data.IDnsEntry['data'] }, context) => {
let currentState = statePartArg.getState() || {};
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.dns.updateDnsEntry(payloadArg.dnsEntryId, payloadArg.dnsEntryData);
currentState = await dataState.dispatchAction(getAllDataAction, null);
currentState = await context.dispatch(getAllDataAction, null);
return currentState;
}
);
export const deleteDnsEntryAction = dataState.createAction(
async (statePartArg, payloadArg: { dnsEntryId: string }) => {
let currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
async (statePartArg, payloadArg: { dnsEntryId: string }, context) => {
let currentState = statePartArg.getState() || {};
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.dns.deleteDnsEntry(payloadArg.dnsEntryId);
currentState = await dataState.dispatchAction(getAllDataAction, null);
currentState = await context.dispatch(getAllDataAction, null);
return currentState;
}
);
// Domain Actions
export const createDomainAction = dataState.createAction(
async (statePartArg, payloadArg: { domainData: plugins.interfaces.data.IDomain['data'] }) => {
let currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
async (statePartArg, payloadArg: { domainData: plugins.interfaces.data.IDomain['data'] }, context) => {
let currentState = statePartArg.getState() || {};
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.domains.createDomain(payloadArg.domainData);
currentState = await dataState.dispatchAction(getAllDataAction, null);
currentState = await context.dispatch(getAllDataAction, null);
return currentState;
}
);
export const updateDomainAction = dataState.createAction(
async (statePartArg, payloadArg: { domainId: string; domainData: plugins.interfaces.data.IDomain['data'] }) => {
let currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
async (statePartArg, payloadArg: { domainId: string; domainData: plugins.interfaces.data.IDomain['data'] }, context) => {
let currentState = statePartArg.getState() || {};
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.domains.updateDomain(payloadArg.domainId, payloadArg.domainData);
currentState = await dataState.dispatchAction(getAllDataAction, null);
currentState = await context.dispatch(getAllDataAction, null);
return currentState;
}
);
export const deleteDomainAction = dataState.createAction(
async (statePartArg, payloadArg: { domainId: string }) => {
let currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
async (statePartArg, payloadArg: { domainId: string }, context) => {
let currentState = statePartArg.getState() || {};
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.domains.deleteDomain(payloadArg.domainId);
currentState = await dataState.dispatchAction(getAllDataAction, null);
currentState = await context.dispatch(getAllDataAction, null);
return currentState;
}
);
export const verifyDomainAction = dataState.createAction(
async (statePartArg, payloadArg: { domainId: string; verificationMethod?: 'dns' | 'http' | 'email' | 'manual' }) => {
let currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
async (statePartArg, payloadArg: { domainId: string; verificationMethod?: 'dns' | 'http' | 'email' | 'manual' }, context) => {
let currentState = statePartArg.getState() || {};
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.domains.verifyDomain(payloadArg.domainId, payloadArg.verificationMethod);
currentState = await dataState.dispatchAction(getAllDataAction, null);
currentState = await context.dispatch(getAllDataAction, null);
return currentState;
}
);
// External Registry Actions
export const createExternalRegistryAction = dataState.createAction(
async (statePartArg, payloadArg: { registryData: plugins.interfaces.data.IExternalRegistry['data'] }) => {
let currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
async (statePartArg, payloadArg: { registryData: plugins.interfaces.data.IExternalRegistry['data'] }, context) => {
let currentState = statePartArg.getState() || {};
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.externalRegistry.createRegistry(payloadArg.registryData);
currentState = await dataState.dispatchAction(getAllDataAction, null);
currentState = await context.dispatch(getAllDataAction, null);
return currentState;
}
);
export const updateExternalRegistryAction = dataState.createAction(
async (statePartArg, payloadArg: { registryId: string; registryData: plugins.interfaces.data.IExternalRegistry['data'] }) => {
let currentState = statePartArg.getState();
async (statePartArg, payloadArg: { registryId: string; registryData: plugins.interfaces.data.IExternalRegistry['data'] }, context) => {
let currentState = statePartArg.getState() || {};
try {
apiClient.identity = loginStatePart.getState().identity;
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.externalRegistry.updateRegistry(payloadArg.registryId, payloadArg.registryData);
currentState = await dataState.dispatchAction(getAllDataAction, null);
currentState = await context.dispatch(getAllDataAction, null);
} catch (err) {
console.error('Failed to update external registry:', err);
}
@@ -469,12 +472,12 @@ export const updateExternalRegistryAction = dataState.createAction(
);
export const deleteExternalRegistryAction = dataState.createAction(
async (statePartArg, payloadArg: { registryId: string }) => {
let currentState = statePartArg.getState();
async (statePartArg, payloadArg: { registryId: string }, context) => {
let currentState = statePartArg.getState() || {};
try {
apiClient.identity = loginStatePart.getState().identity;
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.externalRegistry.deleteRegistry(payloadArg.registryId);
currentState = await dataState.dispatchAction(getAllDataAction, null);
currentState = await context.dispatch(getAllDataAction, null);
} catch (err) {
console.error('Failed to delete external registry:', err);
}
@@ -484,9 +487,9 @@ export const deleteExternalRegistryAction = dataState.createAction(
export const verifyExternalRegistryAction = dataState.createAction(
async (statePartArg, payloadArg: { registryId: string }) => {
let currentState = statePartArg.getState();
let currentState = statePartArg.getState() || {};
try {
apiClient.identity = loginStatePart.getState().identity;
apiClient.identity = loginStatePart.getState()?.identity ?? null;
const result = await apiClient.externalRegistry.verifyRegistry(payloadArg.registryId);
if (result.success && result.registry) {
const regs = (currentState.externalRegistries || []).slice();
@@ -514,8 +517,8 @@ export const verifyExternalRegistryAction = dataState.createAction(
export const taskActions = {
getTasks: dataState.createAction(
async (statePartArg, payloadArg: {}) => {
const currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
const currentState = statePartArg.getState() || {};
apiClient.identity = loginStatePart.getState()?.identity ?? null;
const response = await apiClient.tasks.getTasks();
return {
...currentState,
@@ -526,8 +529,8 @@ export const taskActions = {
getTaskExecutions: dataState.createAction(
async (statePartArg, payloadArg: { filter?: any }) => {
const currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
const currentState = statePartArg.getState() || {};
apiClient.identity = loginStatePart.getState()?.identity ?? null;
const response = await apiClient.tasks.getTaskExecutions(payloadArg.filter);
return {
...currentState,
@@ -538,8 +541,8 @@ export const taskActions = {
getTaskExecutionById: dataState.createAction(
async (statePartArg, payloadArg: { executionId: string }) => {
const currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
const currentState = statePartArg.getState() || {};
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.tasks.getTaskExecutionById(payloadArg.executionId);
return currentState;
}
@@ -547,8 +550,8 @@ export const taskActions = {
triggerTask: dataState.createAction(
async (statePartArg, payloadArg: { taskName: string; userId?: string }) => {
const currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
const currentState = statePartArg.getState() || {};
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.tasks.triggerTask(payloadArg.taskName, payloadArg.userId);
return currentState;
}
@@ -556,8 +559,8 @@ export const taskActions = {
cancelTask: dataState.createAction(
async (statePartArg, payloadArg: { executionId: string }) => {
const currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
const currentState = statePartArg.getState() || {};
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.tasks.cancelTask(payloadArg.executionId);
return currentState;
}
@@ -571,11 +574,12 @@ export const addClusterAction = dataState.createAction(
payloadArg: {
clusterName: string;
setupMode?: 'manual' | 'hetzner' | 'aws' | 'digitalocean';
}
},
context
) => {
let currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
let currentState = statePartArg.getState() || {};
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.cluster.createClusterAdvanced(payloadArg.clusterName, payloadArg.setupMode);
return await dataState.dispatchAction(getAllDataAction, null);
return await context.dispatch(getAllDataAction, null);
}
);
+14 -13
View File
@@ -38,8 +38,8 @@ declare global {
@customElement('cloudly-dashboard')
export class CloudlyDashboard extends DeesElement {
@state() private identity: plugins.interfaces.data.IIdentity;
@state() private data: appstate.IDataState = {
@state() private accessor identity: plugins.interfaces.data.IIdentity | null = null;
@state() private accessor data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
clusters: [],
@@ -188,10 +188,11 @@ export class CloudlyDashboard extends DeesElement {
`;
}
public async firstUpdated() {
const simpleLogin = this.shadowRoot.querySelector('dees-simple-login');
simpleLogin.addEventListener('login', (e: CustomEvent) => {
console.log(e.detail);
this.login(e.detail.data.username, e.detail.data.password);
const simpleLogin = this.shadowRoot!.querySelector('dees-simple-login') as any;
simpleLogin.addEventListener('login', (eventArg: Event) => {
const loginEvent = eventArg as CustomEvent;
console.log(loginEvent.detail);
this.login(loginEvent.detail.data.username, loginEvent.detail.data.password);
});
this.addEventListener('contextmenu', (eventArg) => {
plugins.deesCatalog.DeesContextmenu.openContextMenuWithOptions(eventArg, [
@@ -205,8 +206,8 @@ export class CloudlyDashboard extends DeesElement {
menuOptions: [
{
name: 'close',
iconName: null,
action: async (modalArg) => {
iconName: undefined,
action: async (modalArg: any) => {
await modalArg.destroy();
},
},
@@ -221,14 +222,14 @@ export class CloudlyDashboard extends DeesElement {
const domtools = await this.domtoolsPromise;
const loginState = appstate.loginStatePart.getState();
console.log(loginState);
if (loginState.identity) {
if (loginState?.identity) {
this.identity = loginState.identity;
try {
appstate.apiClient.identity = loginState.identity;
if (!appstate.apiClient['typedsocketClient']) {
await appstate.apiClient.start();
}
try { appstate.apiClient.typedsocketClient.addTag('identity', appstate.apiClient.identity); } catch {}
try { await appstate.apiClient.typedsocketClient.setTag('identity', appstate.apiClient.identity); } catch {}
} catch (e) { console.warn('Failed to initialize API client WS', e); }
await simpleLogin.switchToSlottedContent();
await appstate.dataState.dispatchAction(appstate.getAllDataAction, null);
@@ -238,14 +239,14 @@ export class CloudlyDashboard extends DeesElement {
private async login(username: string, password: string) {
const domtools = await this.domtoolsPromise;
console.log(`attempting to login...`);
const simpleLogin = this.shadowRoot.querySelector('dees-simple-login');
const form = simpleLogin.shadowRoot.querySelector('dees-form');
const simpleLogin = this.shadowRoot!.querySelector('dees-simple-login') as any;
const form = simpleLogin.shadowRoot.querySelector('dees-form') as any;
form.setStatus('pending', 'Logging in...');
const state = await appstate.loginStatePart.dispatchAction(appstate.loginAction, {
username,
password,
});
if (state.identity) {
if (state?.identity) {
console.log('got jwt');
this.identity = state.identity;
form.setStatus('success', 'Logged in!');
+1 -1
View File
@@ -8,7 +8,7 @@ import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-backups')
export class CloudlyViewBackups extends DeesElement {
@state()
private data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
private accessor data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
constructor() {
super();
+2 -2
View File
@@ -25,8 +25,8 @@ const sourcePresetArchitectures: Record<TBaseOsImageSourcePreset, string> = {
@customElement('cloudly-view-baseos')
export class CloudlyViewBaseOs extends DeesElement {
@state() private builds: TBaseOsImageBuild[] = [];
@state() private isLoading = false;
@state() private accessor builds: TBaseOsImageBuild[] = [];
@state() private accessor isLoading = false;
private refreshTimer?: number;
+1 -1
View File
@@ -15,7 +15,7 @@ import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-clusters')
export class CloudlyViewClusters extends DeesElement {
@state()
private data: appstate.IDataState = {} as any;
private accessor data: appstate.IDataState = {} as any;
constructor() {
super();
+1 -1
View File
@@ -8,7 +8,7 @@ import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-dbs')
export class CloudlyViewDbs extends DeesElement {
@state()
private data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
private accessor data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
constructor() {
super();
+1 -2
View File
@@ -15,7 +15,7 @@ import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-deployments')
export class CloudlyViewDeployments extends DeesElement {
@state()
private data: appstate.IDataState = {} as any;
private accessor data: appstate.IDataState = {} as any;
constructor() {
super();
@@ -219,4 +219,3 @@ declare global {
'cloudly-view-deployments': CloudlyViewDeployments;
}
}
+1 -1
View File
@@ -15,7 +15,7 @@ import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-dns')
export class CloudlyViewDns extends DeesElement {
@state()
private data: appstate.IDataState = { secretGroups: [], secretBundles: [], dnsEntries: [], domains: [] } as any;
private accessor data: appstate.IDataState = { secretGroups: [], secretBundles: [], dnsEntries: [], domains: [] } as any;
constructor() {
super();
+1 -1
View File
@@ -8,7 +8,7 @@ import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-domains')
export class CloudlyViewDomains extends DeesElement {
@state()
private data: appstate.IDataState = { secretGroups: [], secretBundles: [], domains: [], dnsEntries: [] } as any;
private accessor data: appstate.IDataState = { secretGroups: [], secretBundles: [], domains: [], dnsEntries: [] } as any;
constructor() {
super();
@@ -15,7 +15,7 @@ import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-externalregistries')
export class CloudlyViewExternalRegistries extends DeesElement {
@state()
private data: appstate.IDataState = { secretGroups: [], secretBundles: [], externalRegistries: [] } as any;
private accessor data: appstate.IDataState = { secretGroups: [], secretBundles: [], externalRegistries: [] } as any;
constructor() {
super();
@@ -115,4 +115,3 @@ export class CloudlyViewExternalRegistries extends DeesElement {
}
declare global { interface HTMLElementTagNameMap { 'cloudly-view-externalregistries': CloudlyViewExternalRegistries; } }
+3 -4
View File
@@ -8,7 +8,7 @@ import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-images')
export class CloudlyViewImages extends DeesElement {
@state()
private data: appstate.IDataState = {} as any;
private accessor data: appstate.IDataState = {} as any;
constructor() {
super();
@@ -86,8 +86,8 @@ export class CloudlyViewImages extends DeesElement {
</dees-form>
`,
menuOptions: [
{ name: 'Cancel', iconName: null, action: async (modalArg: any) => { await modalArg.destroy(); } },
{ name: 'Save', iconName: null, action: async (modalArg: any) => { const data = await modalArg.shadowRoot.querySelector('dees-form').collectFormData(); console.log(data); } },
{ name: 'Cancel', iconName: undefined, action: async (modalArg: any) => { await modalArg.destroy(); } },
{ name: 'Save', iconName: undefined, action: async (modalArg: any) => { const data = await modalArg.shadowRoot.querySelector('dees-form').collectFormData(); console.log(data); } },
],
});
},
@@ -139,4 +139,3 @@ declare global {
'cloudly-view-images': CloudlyViewImages;
}
}
+1 -1
View File
@@ -15,7 +15,7 @@ import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-logs')
export class CloudlyViewLogs extends DeesElement {
@state()
private data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
private accessor data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
constructor() {
super();
+1 -1
View File
@@ -15,7 +15,7 @@ import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-mails')
export class CloudlyViewMails extends DeesElement {
@state()
private data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
private accessor data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
constructor() {
super();
+1 -2
View File
@@ -14,7 +14,7 @@ import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-overview')
export class CloudlyViewOverview extends DeesElement {
@state()
private data: appstate.IDataState = {
private accessor data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
@@ -68,4 +68,3 @@ declare global {
'cloudly-view-overview': CloudlyViewOverview;
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-s3')
export class CloudlyViewS3 extends DeesElement {
@state()
private data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
private accessor data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
constructor() {
super();
+2 -3
View File
@@ -15,7 +15,7 @@ import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-secretbundles')
export class CloudlyViewSecretBundles extends DeesElement {
@state()
private data: appstate.IDataState = {} as any;
private accessor data: appstate.IDataState = {} as any;
constructor() {
super();
@@ -39,7 +39,7 @@ export class CloudlyViewSecretBundles extends DeesElement {
const secretGroupIds = itemArg.data.includedSecretGroupIds;
let secretGroupNames: string[] = [];
for (const secretGroupId of secretGroupIds) {
const secretGroup = this.data.secretGroups.find((secretGroupArg: any) => secretGroupArg.id === secretGroupId);
const secretGroup = this.data.secretGroups?.find((secretGroupArg: any) => secretGroupArg.id === secretGroupId);
if (secretGroup) { secretGroupNames.push(secretGroup.data.name); }
}
return secretGroupNames.join(', ');
@@ -73,4 +73,3 @@ export class CloudlyViewSecretBundles extends DeesElement {
}
declare global { interface HTMLElementTagNameMap { 'cloudly-view-secretbundles': CloudlyViewSecretBundles; } }
+4 -5
View File
@@ -8,7 +8,7 @@ import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-secretsgroups')
export class CloudlyViewSecretGroups extends DeesElement {
@state()
private data: appstate.IDataState = {} as any;
private accessor data: appstate.IDataState = {} as any;
constructor() {
super();
@@ -31,7 +31,7 @@ export class CloudlyViewSecretGroups extends DeesElement {
priority: secretGroup.data.priority,
tags: html`<dees-chips .selectionMode=${'none'} .selectableChips=${secretGroup.data.tags}></dees-chips>`,
key: secretGroup.data.key,
history: (() => { const allHistory = []; for (const environment in secretGroup.data.environments) { allHistory.push(...secretGroup.data.environments[environment].history); } return allHistory.length; })(),
history: (() => { const allHistory: Array<{ timestamp: string; value: string }> = []; for (const environment in secretGroup.data.environments) { allHistory.push(...secretGroup.data.environments[environment].history); } return allHistory.length; })(),
};
}}
.dataActions=${[
@@ -44,7 +44,7 @@ export class CloudlyViewSecretGroups extends DeesElement {
<dees-table heading1=${'Environments'} heading2=${'keys need to be unique'} key="environments" .data=${[{ environment: 'production', value: '' }, { environment: 'staging', value: '' }]} .dataActions=${[{ name: 'add environment', iconName: 'plus', type: ['footer'], actionFunc: async (dataArg: any) => { dataArg.table.data.push({ environment: 'new environment', value: '' }); dataArg.table.requestUpdate('data'); } }, { name: 'delete environment', iconName: 'trash', type: ['inRow'], actionFunc: async (dataArg: any) => { dataArg.table.data.splice(dataArg.table.data.indexOf(dataArg.item), 1); dataArg.table.requestUpdate('data'); } }] as plugins.deesCatalog.ITableAction[]} .editableFields=${['environment', 'value']}>
</dees-table>
</dees-form>
`, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'save', action: async (modalArg: any) => { const deesForm = modalArg.shadowRoot.querySelector('dees-form'); const formData = await deesForm.collectFormData(); const environments: plugins.interfaces.data.ISecretGroup['data']['environments'] = {}; for (const itemArg of formData['environments'] as any[]) { environments[itemArg.environment] = { value: itemArg.value, history: [], lastUpdated: Date.now(), }; } await appstate.dataState.dispatchAction(appstate.createSecretGroupAction, { id: null, data: { name: formData['data.name'] as string, description: formData['data.description'] as string, key: formData['data.key'] as string, environments, tags: [], }, }); await modalArg.destroy(); } } ] });
`, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'save', action: async (modalArg: any) => { const deesForm = modalArg.shadowRoot.querySelector('dees-form'); const formData = await deesForm.collectFormData(); const environments: plugins.interfaces.data.ISecretGroup['data']['environments'] = {}; for (const itemArg of formData['environments'] as any[]) { environments[itemArg.environment] = { value: itemArg.value, history: [], lastUpdated: Date.now(), }; } await appstate.dataState.dispatchAction(appstate.createSecretGroupAction, { data: { name: formData['data.name'] as string, description: formData['data.description'] as string, key: formData['data.key'] as string, environments, tags: [], }, }); await modalArg.destroy(); } } ] });
} },
{ name: 'edit', type: ['contextmenu', 'inRow', 'doubleClick'], iconName: 'penToSquare', actionFunc: async (dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>) => {
const environmentsArray: Array<plugins.interfaces.data.ISecretGroup['data']['environments'][any] & { environment: string; }> = [];
@@ -58,7 +58,7 @@ export class CloudlyViewSecretGroups extends DeesElement {
<dees-table .key=${'environments'} .heading1=${'Environments'} .heading2=${'double-click to edit values'} .data=${environmentsArray.map((itemArg) => ({ environment: itemArg.environment, value: itemArg.value, }))} .editableFields=${['environment', 'value']} .dataActions=${[{ name: 'delete', iconName: 'trash', type: ['inRow'], actionFunc: async (actionDataArg: any) => { actionDataArg.table.data.splice(actionDataArg.table.data.indexOf(actionDataArg.item), 1); } }] as plugins.deesCatalog.ITableAction[]}>
</dees-table>
</dees-form>
`, menuOptions: [ { name: 'Cancel', iconName: null, action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'Save', iconName: null, action: async (modalArg: any) => { const data = await modalArg.shadowRoot.querySelector('dees-form').collectFormData(); console.log(data); } } ] });
`, menuOptions: [ { name: 'Cancel', iconName: undefined, action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'Save', iconName: undefined, action: async (modalArg: any) => { const data = await modalArg.shadowRoot.querySelector('dees-form').collectFormData(); console.log(data); } } ] });
} },
{ name: 'history', iconName: 'clockRotateLeft', type: ['contextmenu', 'inRow'], actionFunc: async (dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>) => {
const historyArray: Array<{ environment: string; value: string; }> = []; for (const environment of Object.keys(dataArg.item.data.environments)) { for (const historyItem of dataArg.item.data.environments[environment].history) { historyArray.push({ environment, value: historyItem.value, }); } }
@@ -74,4 +74,3 @@ export class CloudlyViewSecretGroups extends DeesElement {
}
declare global { interface HTMLElementTagNameMap { 'cloudly-view-secretsgroups': CloudlyViewSecretGroups; } }
+1 -1
View File
@@ -15,7 +15,7 @@ import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-services')
export class CloudlyViewServices extends DeesElement {
@state()
private data: appstate.IDataState = {} as any;
private accessor data: appstate.IDataState = {} as any;
constructor() {
super();
+3 -3
View File
@@ -15,13 +15,13 @@ import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-settings')
export class CloudlyViewSettings extends DeesElement {
@state()
private settings: plugins.interfaces.data.ICloudlySettingsMasked = {} as any;
private accessor settings: plugins.interfaces.data.ICloudlySettingsMasked = {} as any;
@state()
private isLoading = false;
private accessor isLoading = false;
@state()
private testResults: {[key: string]: {success: boolean; message: string}} = {};
private accessor testResults: {[key: string]: {success: boolean; message: string}} = {};
constructor() {
super();
+10 -10
View File
@@ -18,29 +18,29 @@ import { formatCronFriendly, formatDate, formatDuration } from './utils.js';
@customElement('cloudly-view-tasks')
export class CloudlyViewTasks extends DeesElement {
@state()
private data: appstate.IDataState = {} as any;
private accessor data: appstate.IDataState = {} as any;
@state()
private selectedExecution: plugins.interfaces.data.ITaskExecution | null = null;
private accessor selectedExecution: plugins.interfaces.data.ITaskExecution | null = null;
@state()
private loading = false;
private accessor loading = false;
@state()
private filterStatus: string = 'all';
private accessor filterStatus: string = 'all';
@state()
private searchQuery: string = '';
private accessor searchQuery: string = '';
@state()
private categoryFilter: string = 'all';
private accessor categoryFilter: string = 'all';
@state()
private autoRefresh: boolean = true;
private accessor autoRefresh: boolean = true;
private _refreshHandle: any = null;
@state()
private canceling: Record<string, boolean> = {};
private accessor canceling: Record<string, boolean> = {};
constructor() {
super();
@@ -143,7 +143,7 @@ export class CloudlyViewTasks extends DeesElement {
});
} catch (error) {
console.error('Failed to trigger task:', error);
plugins.deesCatalog.DeesToast.createAndShow({ message: `Failed to trigger: ${error.message}`, type: 'error' });
plugins.deesCatalog.DeesToast.createAndShow({ message: `Failed to trigger: ${error instanceof Error ? error.message : String(error)}`, type: 'error' });
}
}
@@ -165,7 +165,7 @@ export class CloudlyViewTasks extends DeesElement {
}
} catch (err) {
console.error('Failed to cancel task:', err);
plugins.deesCatalog.DeesToast.createAndShow({ message: `Cancel failed: ${err.message}`, type: 'error' });
plugins.deesCatalog.DeesToast.createAndShow({ message: `Cancel failed: ${err instanceof Error ? err.message : String(err)}`, type: 'error' });
}
}
@@ -3,7 +3,7 @@ import { formatDate, formatDuration } from '../utils.js';
@customElement('cloudly-execution-details')
export class CloudlyExecutionDetails extends DeesElement {
@property({ type: Object }) execution: any;
@property({ type: Object }) accessor execution: any = undefined;
public static styles = [
cssManager.defaultStyles,
@@ -90,4 +90,3 @@ declare global {
'cloudly-execution-details': CloudlyExecutionDetails;
}
}
@@ -3,15 +3,15 @@ import { formatCronFriendly, formatDuration, formatRelativeTime, getCategoryHue,
@customElement('cloudly-task-panel')
export class CloudlyTaskPanel extends DeesElement {
@property({ type: Object }) task: any;
@property({ type: Array }) executions: any[] = [];
@property({ type: Object }) canceling: Record<string, boolean> = {};
@property({ type: Object }) accessor task: any = undefined;
@property({ type: Array }) accessor executions: any[] = [];
@property({ type: Object }) accessor canceling: Record<string, boolean> = {};
// Callbacks provided by parent view
@property({ attribute: false }) onRun?: (taskName: string) => void;
@property({ attribute: false }) onCancel?: (taskName: string) => void;
@property({ attribute: false }) onOpenDetails?: (execution: any) => void;
@property({ attribute: false }) onOpenLogs?: (execution: any) => void;
@property({ attribute: false }) accessor onRun: ((taskName: string) => void) | undefined = undefined;
@property({ attribute: false }) accessor onCancel: ((taskName: string) => void) | undefined = undefined;
@property({ attribute: false }) accessor onOpenDetails: ((execution: any) => void) | undefined = undefined;
@property({ attribute: false }) accessor onOpenLogs: ((execution: any) => void) | undefined = undefined;
public static styles = [
cssManager.defaultStyles,
@@ -203,4 +203,3 @@ declare global {
'cloudly-task-panel': CloudlyTaskPanel;
}
}
+4 -1
View File
@@ -1,10 +1,13 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"useDefineForClassFields": false,
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"types": [
"node"
],
"ignoreDeprecations": "6.0",
"esModuleInterop": true,
"verbatimModuleSyntax": true,
"baseUrl": ".",