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:
@@ -6,6 +6,61 @@
|
|||||||
},
|
},
|
||||||
"platforms": ["linux/amd64", "linux/arm64"]
|
"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": {
|
"@git.zone/cli": {
|
||||||
"projectType": "service",
|
"projectType": "service",
|
||||||
"module": {
|
"module": {
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"json.schemas": [
|
"json.schemas": [
|
||||||
{
|
{
|
||||||
"fileMatch": ["/npmextra.json"],
|
"fileMatch": ["/.smartconfig.json"],
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -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
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/cloudly",
|
"name": "@serve.zone/cloudly",
|
||||||
"version": "5.4.0",
|
"version": "5.5.1",
|
||||||
"private": false,
|
"private": true,
|
||||||
"description": "A comprehensive tool for managing containerized applications across multiple cloud providers using Docker Swarmkit, featuring web, CLI, and API interfaces.",
|
"description": "A comprehensive tool for managing containerized applications across multiple cloud providers using Docker Swarmkit, featuring web, CLI, and API interfaces.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -13,86 +13,86 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "(tstest test/ --verbose --logfile --timeout 120)",
|
"test": "(tstest test/ --verbose --logfile --timeout 120)",
|
||||||
"build": "tsbuild tsfolders --web --allowimplicitany && tsbundle website --production",
|
"build": "tsbuild tsfolders && tsbundle",
|
||||||
"build:docker": "tsdocker build --verbose",
|
"build:docker": "tsdocker build --verbose",
|
||||||
"start": "node cli.js",
|
"start": "node cli.js",
|
||||||
"startTs": "node cli.ts.js",
|
"startTs": "node cli.ts.js",
|
||||||
"watch": "tswatch website",
|
"watch": "tswatch",
|
||||||
"release:docker": "tsdocker push --verbose",
|
"release:docker": "tsdocker push --verbose",
|
||||||
"publish": "tspublish",
|
"publish": "tspublish",
|
||||||
"docs": "tsdoc aidoc"
|
"docs": "tsdoc aidoc"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.6.8",
|
"@git.zone/tsbuild": "^4.4.0",
|
||||||
"@git.zone/tsbundle": "^2.5.1",
|
"@git.zone/tsbundle": "^2.10.1",
|
||||||
"@git.zone/tsdocker": "^2.2.4",
|
"@git.zone/tsdoc": "^2.0.3",
|
||||||
"@git.zone/tsdoc": "^1.5.2",
|
"@git.zone/tsdocker": "^2.2.6",
|
||||||
"@git.zone/tspublish": "^1.10.3",
|
"@git.zone/tspublish": "^1.11.6",
|
||||||
"@git.zone/tstest": "^2.3.8",
|
"@git.zone/tstest": "^3.6.5",
|
||||||
"@git.zone/tswatch": "^2.2.1",
|
"@git.zone/tswatch": "^3.3.3",
|
||||||
"@push.rocks/smartnetwork": "^4.4.0",
|
"@push.rocks/smartnetwork": "^4.7.1",
|
||||||
"@types/node": "^22.0.0"
|
"@types/node": "^25.6.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@api.global/typedrequest": "3.1.10",
|
"@api.global/typedrequest": "3.3.1",
|
||||||
"@api.global/typedrequest-interfaces": "^3.0.19",
|
"@api.global/typedrequest-interfaces": "^3.0.19",
|
||||||
"@api.global/typedserver": "^3.0.79",
|
"@api.global/typedserver": "^8.4.6",
|
||||||
"@api.global/typedsocket": "^3.0.1",
|
"@api.global/typedsocket": "^4.1.3",
|
||||||
"@apiclient.xyz/cloudflare": "^6.4.1",
|
"@apiclient.xyz/cloudflare": "^7.1.0",
|
||||||
"@apiclient.xyz/docker": "^1.3.5",
|
"@apiclient.xyz/docker": "^5.1.4",
|
||||||
"@apiclient.xyz/hetznercloud": "^1.2.0",
|
"@apiclient.xyz/hetznercloud": "^1.2.1",
|
||||||
"@apiclient.xyz/slack": "^3.0.9",
|
"@apiclient.xyz/slack": "^3.0.9",
|
||||||
"@design.estate/dees-catalog": "^1.11.3",
|
"@design.estate/dees-catalog": "^3.81.0",
|
||||||
"@design.estate/dees-domtools": "^2.3.3",
|
"@design.estate/dees-domtools": "^2.5.6",
|
||||||
"@design.estate/dees-element": "^2.1.2",
|
"@design.estate/dees-element": "^2.2.4",
|
||||||
"@git.zone/tsrun": "^1.3.3",
|
"@git.zone/tsrun": "^2.0.3",
|
||||||
"@push.rocks/early": "^4.0.3",
|
"@push.rocks/early": "^4.0.4",
|
||||||
"@push.rocks/npmextra": "^5.3.3",
|
"@push.rocks/projectinfo": "^5.1.0",
|
||||||
"@push.rocks/projectinfo": "^5.0.1",
|
"@push.rocks/qenv": "^6.1.4",
|
||||||
"@push.rocks/qenv": "^6.1.3",
|
"@push.rocks/smartacme": "^9.5.0",
|
||||||
"@push.rocks/smartacme": "^8.0.0",
|
"@push.rocks/smartbucket": "^4.6.1",
|
||||||
"@push.rocks/smartbucket": "^3.3.10",
|
"@push.rocks/smartcli": "^4.0.21",
|
||||||
"@push.rocks/smartcli": "^4.0.11",
|
"@push.rocks/smartclickhouse": "^2.2.1",
|
||||||
"@push.rocks/smartclickhouse": "^2.0.17",
|
"@push.rocks/smartconfig": "^6.1.1",
|
||||||
"@push.rocks/smartdata": "^5.16.4",
|
"@push.rocks/smartdata": "^7.1.7",
|
||||||
"@push.rocks/smartdelay": "^3.0.5",
|
"@push.rocks/smartdelay": "^3.1.0",
|
||||||
"@push.rocks/smartexit": "^1.0.23",
|
"@push.rocks/smartexit": "^2.0.3",
|
||||||
"@push.rocks/smartexpect": "^2.5.0",
|
"@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/smartguard": "^3.1.0",
|
||||||
"@push.rocks/smartjson": "^5.2.0",
|
"@push.rocks/smartjson": "^6.0.1",
|
||||||
"@push.rocks/smartjwt": "^2.2.2",
|
"@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-destination-clickhouse": "^1.0.13",
|
||||||
"@push.rocks/smartlog-interfaces": "^3.0.2",
|
"@push.rocks/smartlog-interfaces": "^3.0.2",
|
||||||
"@push.rocks/smartpath": "^6.0.0",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartpromise": "^4.2.3",
|
"@push.rocks/smartpromise": "^4.2.4",
|
||||||
"@push.rocks/smartregistry": "^2.9.1",
|
"@push.rocks/smartregistry": "^2.9.2",
|
||||||
"@push.rocks/smartrequest": "^4.3.1",
|
"@push.rocks/smartrequest": "^5.0.3",
|
||||||
"@push.rocks/smartsamba": "^0.2.0",
|
|
||||||
"@push.rocks/smartrx": "^3.0.10",
|
"@push.rocks/smartrx": "^3.0.10",
|
||||||
"@push.rocks/smartssh": "^2.0.1",
|
"@push.rocks/smartsamba": "^0.2.0",
|
||||||
"@push.rocks/smartstate": "^2.0.27",
|
"@push.rocks/smartssh": "^2.1.0",
|
||||||
"@push.rocks/smartstream": "^3.2.5",
|
"@push.rocks/smartstate": "^2.3.1",
|
||||||
"@push.rocks/smartstring": "^4.1.0",
|
"@push.rocks/smartstream": "^3.4.2",
|
||||||
|
"@push.rocks/smartstring": "^4.1.1",
|
||||||
"@push.rocks/smartunique": "^3.0.9",
|
"@push.rocks/smartunique": "^3.0.9",
|
||||||
"@push.rocks/taskbuffer": "^3.4.0",
|
"@push.rocks/taskbuffer": "^8.0.2",
|
||||||
"@push.rocks/webjwt": "^1.0.9",
|
"@push.rocks/webjwt": "^1.0.10",
|
||||||
"@serve.zone/api": "^5.3.4",
|
"@serve.zone/api": "^5.3.7",
|
||||||
"@serve.zone/interfaces": "^5.5.0",
|
"@serve.zone/interfaces": "^5.6.0",
|
||||||
"@tsclass/tsclass": "^9.2.0"
|
"@tsclass/tsclass": "^9.5.1"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"ts/**/*",
|
"ts/**/*",
|
||||||
|
"ts_cliclient/**/*",
|
||||||
"ts_web/**/*",
|
"ts_web/**/*",
|
||||||
"dist/**/*",
|
"dist_serve/**/*",
|
||||||
"dist_*/**/*",
|
|
||||||
"dist_ts/**/*",
|
"dist_ts/**/*",
|
||||||
|
"dist_ts_cliclient/**/*",
|
||||||
"dist_ts_web/**/*",
|
"dist_ts_web/**/*",
|
||||||
"assets/**/*",
|
"assets/**/*",
|
||||||
"cli.js",
|
"cli.js",
|
||||||
".smartconfig.json",
|
".smartconfig.json",
|
||||||
"npmextra.json",
|
|
||||||
"readme.md"
|
"readme.md"
|
||||||
],
|
],
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
@@ -134,10 +134,5 @@
|
|||||||
"backend",
|
"backend",
|
||||||
"security"
|
"security"
|
||||||
],
|
],
|
||||||
"pnpm": {
|
|
||||||
"overrides": {
|
|
||||||
"@push.rocks/lik": "6.2.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
|
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+3201
-5800
File diff suppressed because it is too large
Load Diff
@@ -30,15 +30,17 @@ Cloudly currently coordinates these areas:
|
|||||||
- **Authentication and identity**: human admin login, JWT identities, machine tokens, and cluster identities.
|
- **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.
|
- **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.
|
- **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`.
|
- **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.
|
- **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.
|
- **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.
|
- **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.
|
- **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.
|
- **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
|
## 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. |
|
| `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. |
|
| `CloudlyBaseOsManager` | BaseOS registration, heartbeat, image build orchestration, worker selection, and artifact downloads. |
|
||||||
| `CloudlyBackupManager` | Service backup/restore orchestration and remote archive object replication. |
|
| `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
|
## 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:
|
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. |
|
| `corebuildWorkersJson` | JSON array of CoreBuild worker URLs or `{ "url", "token", "id" }` objects. |
|
||||||
| `corebuildWorkerUrl` / `corebuildWorkerToken` | Legacy single-worker CoreBuild settings. |
|
| `corebuildWorkerUrl` / `corebuildWorkerToken` | Legacy single-worker CoreBuild settings. |
|
||||||
| `dcrouterGatewayUrl` / `dcrouterGatewayApiToken` | Optional external gateway integration for domain and route sync. |
|
| `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
|
## 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.
|
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`:
|
Typical consumers use `@serve.zone/api`:
|
||||||
|
|
||||||
```ts
|
```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:
|
For Cloudly-managed services, `getServiceRegistryTarget()` creates stable registry targets like:
|
||||||
|
|
||||||
```text
|
```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 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
|
## BaseOS and CoreBuild
|
||||||
|
|
||||||
Cloudly can manage BaseOS nodes and image builds:
|
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.
|
- A configured `baseosJoinToken` accepts generic device enrollment.
|
||||||
- BaseOS image builds create one-time provisioning tokens that are embedded in generated images.
|
- 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`.
|
- 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.
|
- 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.
|
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 archive replication through `prepareBackupReplication`, `uploadBackupArchiveObject`, `completeBackupReplication`, `getBackupArchiveManifest`, and `downloadBackupArchiveObject`.
|
||||||
- Optional scheduled `backup-all-services` task when `CLOUDLY_BACKUP_CRON` is set.
|
- 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
|
## 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.
|
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
|
## 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
|
## License and Legal Information
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
import { Qenv } from '@push.rocks/qenv';
|
||||||
const testQenv = new Qenv('./', './.nogit/');
|
const testQenv = new Qenv('./', './.nogit/');
|
||||||
|
|
||||||
@@ -12,17 +12,15 @@ let testCloudly: cloudly.Cloudly;
|
|||||||
|
|
||||||
tap.test('first test', async () => {
|
tap.test('first test', async () => {
|
||||||
const cloudlyConfig: cloudly.ICloudlyConfig = {
|
const cloudlyConfig: cloudly.ICloudlyConfig = {
|
||||||
cfToken: await testQenv.getEnvVarOnDemand('CF_TOKEN'),
|
|
||||||
environment: 'integration',
|
environment: 'integration',
|
||||||
letsEncryptEmail: await testQenv.getEnvVarOnDemand('LETSENCRYPT_EMAIL'),
|
letsEncryptEmail: await testQenv.getEnvVarOnDemandStrict('LETSENCRYPT_EMAIL'),
|
||||||
publicUrl: await testQenv.getEnvVarOnDemand('SERVEZONE_URL'),
|
publicUrl: await testQenv.getEnvVarOnDemandStrict('SERVEZONE_URL'),
|
||||||
publicPort: await testQenv.getEnvVarOnDemand('SERVEZONE_PORT'),
|
publicPort: await testQenv.getEnvVarOnDemandStrict('SERVEZONE_PORT'),
|
||||||
mongoDescriptor: {
|
mongoDescriptor: {
|
||||||
mongoDbName: await testQenv.getEnvVarOnDemand('MONGODB_DATABASE'),
|
mongoDbName: await testQenv.getEnvVarOnDemandStrict('MONGODB_DATABASE'),
|
||||||
mongoDbPass: await testQenv.getEnvVarOnDemand('MONGODB_PASSWORD'),
|
mongoDbPass: await testQenv.getEnvVarOnDemandStrict('MONGODB_PASSWORD'),
|
||||||
mongoDbUrl: await testQenv.getEnvVarOnDemand('MONGODB_URL')
|
mongoDbUrl: await testQenv.getEnvVarOnDemandStrict('MONGODB_URL')
|
||||||
},
|
},
|
||||||
digitalOceanToken: await testQenv.getEnvVarOnDemand('DIGITALOCEAN_TOKEN')
|
|
||||||
};
|
};
|
||||||
testCloudly = new cloudly.Cloudly(cloudlyConfig);
|
testCloudly = new cloudly.Cloudly(cloudlyConfig);
|
||||||
expect(testCloudly).toBeInstanceOf(cloudly.Cloudly);
|
expect(testCloudly).toBeInstanceOf(cloudly.Cloudly);
|
||||||
@@ -36,4 +34,4 @@ tap.test('should end the service', async () => {
|
|||||||
await testCloudly.stop();
|
await testCloudly.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
||||||
|
|||||||
@@ -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';
|
import { Qenv } from '@push.rocks/qenv';
|
||||||
const testQenv = new Qenv('./', './.nogit/');
|
const testQenv = new Qenv('./', './.nogit/');
|
||||||
|
|
||||||
delete process.env.CLI_CALL;
|
delete process.env.CLI_CALL;
|
||||||
|
|
||||||
import * as cloudly from '../ts/index';
|
import * as cloudly from '../ts/index.js';
|
||||||
|
|
||||||
process.env.TESTING_CLOUDLY = 'true';
|
process.env.TESTING_CLOUDLY = 'true';
|
||||||
|
|
||||||
@@ -12,20 +12,18 @@ let testCloudly: cloudly.Cloudly;
|
|||||||
|
|
||||||
tap.test('first test', async () => {
|
tap.test('first test', async () => {
|
||||||
const cloudlyConfig: cloudly.ICloudlyConfig = {
|
const cloudlyConfig: cloudly.ICloudlyConfig = {
|
||||||
cfToken: testQenv.getEnvVarOnDemand('CF_TOKEN'),
|
|
||||||
environment: 'integration',
|
environment: 'integration',
|
||||||
letsEncryptEmail: testQenv.getEnvVarOnDemand('LETSENCRYPT_EMAIL'),
|
letsEncryptEmail: await testQenv.getEnvVarOnDemandStrict('LETSENCRYPT_EMAIL'),
|
||||||
publicUrl: testQenv.getEnvVarOnDemand('SERVEZONE_URL'),
|
publicUrl: await testQenv.getEnvVarOnDemandStrict('SERVEZONE_URL'),
|
||||||
publicPort: testQenv.getEnvVarOnDemand('SERVEZONE_PORT'),
|
publicPort: await testQenv.getEnvVarOnDemandStrict('SERVEZONE_PORT'),
|
||||||
mongoDescriptor: {
|
mongoDescriptor: {
|
||||||
mongoDbName: testQenv.getEnvVarOnDemand('MONGODB_DATABASE'),
|
mongoDbName: await testQenv.getEnvVarOnDemandStrict('MONGODB_DATABASE'),
|
||||||
mongoDbPass: testQenv.getEnvVarOnDemand('MONGODB_PASSWORD'),
|
mongoDbPass: await testQenv.getEnvVarOnDemandStrict('MONGODB_PASSWORD'),
|
||||||
mongoDbUrl: testQenv.getEnvVarOnDemand('MONGODB_URL')
|
mongoDbUrl: await testQenv.getEnvVarOnDemandStrict('MONGODB_URL')
|
||||||
},
|
},
|
||||||
digitalOceanToken: testQenv.getEnvVarOnDemand('DIGITALOCEAN_TOKEN')
|
|
||||||
};
|
};
|
||||||
testCloudly = new cloudly.Cloudly(cloudlyConfig);
|
testCloudly = new cloudly.Cloudly(cloudlyConfig);
|
||||||
expect(testCloudly).to.be.instanceof(cloudly.Cloudly);
|
expect(testCloudly).toBeInstanceOf(cloudly.Cloudly);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should init servezone', async () => {
|
tap.test('should init servezone', async () => {
|
||||||
@@ -36,4 +34,4 @@ tap.test('should end the service', async () => {
|
|||||||
await testCloudly.stop();
|
await testCloudly.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { Qenv } from '@push.rocks/qenv';
|
import { Qenv } from '@push.rocks/qenv';
|
||||||
import { SmartNetwork } from '@push.rocks/smartnetwork';
|
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/');
|
const testQenv = new Qenv('./', './.nogit/');
|
||||||
|
|
||||||
@@ -24,10 +27,10 @@ const smartmongo = await tapNodeTools.createSmartmongo();
|
|||||||
stopFunctions.push(async () => {
|
stopFunctions.push(async () => {
|
||||||
await smartmongo.stopAndDumpToDir('./.nogit/mongodump');
|
await smartmongo.stopAndDumpToDir('./.nogit/mongodump');
|
||||||
});
|
});
|
||||||
const smarts3 = await tapNodeTools.createSmarts3();
|
const smartstorage = await tapNodeTools.createSmartStorage();
|
||||||
await smarts3.createBucket('cloudly_test_bucket');
|
await smartstorage.createBucket('cloudly_test_bucket');
|
||||||
stopFunctions.push(async () => {
|
stopFunctions.push(async () => {
|
||||||
await smarts3.stop();
|
await smartstorage.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
export const testCloudlyAdminAccount = {
|
export const testCloudlyAdminAccount = {
|
||||||
@@ -36,13 +39,12 @@ export const testCloudlyAdminAccount = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const testCloudlyConfig: cloudly.ICloudlyConfig = {
|
export const testCloudlyConfig: cloudly.ICloudlyConfig = {
|
||||||
cfToken: await testQenv.getEnvVarOnDemand('CF_TOKEN'),
|
|
||||||
environment: 'integration',
|
environment: 'integration',
|
||||||
letsEncryptEmail: 'test@serve.zone',
|
letsEncryptEmail: 'test@serve.zone',
|
||||||
publicUrl: '127.0.0.1',
|
publicUrl: '127.0.0.1',
|
||||||
publicPort: await getPublicPort(),
|
publicPort: await getPublicPort(),
|
||||||
mongoDescriptor: await smartmongo.getMongoDescriptor(),
|
mongoDescriptor: await smartmongo.getMongoDescriptor(),
|
||||||
s3Descriptor: await smarts3.getS3Descriptor({
|
s3Descriptor: await smartstorage.getStorageDescriptor({
|
||||||
bucketName: 'cloudly_test_bucket'
|
bucketName: 'cloudly_test_bucket'
|
||||||
}),
|
}),
|
||||||
sslMode: 'none',
|
sslMode: 'none',
|
||||||
|
|||||||
+13
-7
@@ -7,6 +7,15 @@ import * as cloudlyApiClient from '@serve.zone/api';
|
|||||||
let testCloudly: cloudly.Cloudly;
|
let testCloudly: cloudly.Cloudly;
|
||||||
let testClient: cloudlyApiClient.CloudlyApiClient;
|
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 () => {
|
tap.preTask('should start cloudly', async () => {
|
||||||
testCloudly = await helpers.createCloudly();
|
testCloudly = await helpers.createCloudly();
|
||||||
await testCloudly.start();
|
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(` - Username: ${machineUser.data.username}`);
|
||||||
console.log(` - Role: ${machineUser.data.role}`);
|
console.log(` - Role: ${machineUser.data.role}`);
|
||||||
console.log(` - Token: 'test'`);
|
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();
|
await machineUser.save();
|
||||||
console.log('✅ PreTask: First machine user saved successfully');
|
console.log('✅ PreTask: First machine user saved successfully');
|
||||||
});
|
});
|
||||||
@@ -78,8 +87,7 @@ tap.test('should get an identity', async () => {
|
|||||||
expect(identity).toBeTruthy();
|
expect(identity).toBeTruthy();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Failed to get identity:');
|
console.error('❌ Failed to get identity:');
|
||||||
console.error(` - Error message: ${error.message}`);
|
logErrorDetails(error);
|
||||||
console.error(` - Error stack:`, error.stack);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -378,8 +386,7 @@ tap.test('should create and upload an image', async () => {
|
|||||||
expect(image).toBeTruthy();
|
expect(image).toBeTruthy();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Failed to create image:');
|
console.error('❌ Failed to create image:');
|
||||||
console.error(` - Error message: ${error.message}`);
|
logErrorDetails(error);
|
||||||
console.error(` - Error stack:`, error.stack);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -398,8 +405,7 @@ tap.test('should upload an image version', async () => {
|
|||||||
console.log('✅ Image version uploaded successfully');
|
console.log('✅ Image version uploaded successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Failed to upload image version:');
|
console.error('❌ Failed to upload image version:');
|
||||||
console.error(` - Error message: ${error.message}`);
|
logErrorDetails(error);
|
||||||
console.error(` - Error stack:`, error.stack);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -20,4 +20,4 @@ tap.test('should end the service', async (tools) => {
|
|||||||
tools.delayFor(1000).then(() => process.exit());
|
tools.delayFor(1000).then(() => process.exit());
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
export default tap.start();
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export class Cloudly {
|
|||||||
|
|
||||||
private readyDeferred = new plugins.smartpromise.Deferred();
|
private readyDeferred = new plugins.smartpromise.Deferred();
|
||||||
|
|
||||||
private configOptions: plugins.servezoneInterfaces.data.ICloudlyConfig;
|
private configOptions?: plugins.servezoneInterfaces.data.ICloudlyConfig;
|
||||||
constructor(configArg?: plugins.servezoneInterfaces.data.ICloudlyConfig) {
|
constructor(configArg?: plugins.servezoneInterfaces.data.ICloudlyConfig) {
|
||||||
this.configOptions = configArg;
|
this.configOptions = configArg;
|
||||||
this.cloudlyInfo = new CloudlyInfo(this);
|
this.cloudlyInfo = new CloudlyInfo(this);
|
||||||
@@ -148,7 +148,9 @@ export class Cloudly {
|
|||||||
await this.domainManager.init();
|
await this.domainManager.init();
|
||||||
|
|
||||||
await this.cloudflareConnector.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.clusterManager.init();
|
||||||
await this.server.start();
|
await this.server.start();
|
||||||
this.readyDeferred.resolve();
|
this.readyDeferred.resolve();
|
||||||
@@ -163,7 +165,9 @@ export class Cloudly {
|
|||||||
*/
|
*/
|
||||||
public async stop() {
|
public async stop() {
|
||||||
await this.server.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.mongodbConnector.stop();
|
||||||
await this.secretManager.stop();
|
await this.secretManager.stop();
|
||||||
await this.serviceManager.stop();
|
await this.serviceManager.stop();
|
||||||
|
|||||||
@@ -9,5 +9,5 @@ export class CloudlyInfo {
|
|||||||
this.cloudlyRef = cloudlyRefArg;
|
this.cloudlyRef = cloudlyRefArg;
|
||||||
}
|
}
|
||||||
|
|
||||||
public projectInfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
|
public projectInfo = plugins.projectinfo.ProjectInfo.create(paths.packageDir);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import type { Cloudly } from './classes.cloudly.js';
|
|||||||
*/
|
*/
|
||||||
export class CloudlyConfig {
|
export class CloudlyConfig {
|
||||||
public cloudlyRef: Cloudly;
|
public cloudlyRef: Cloudly;
|
||||||
public appData: plugins.npmextra.AppData<plugins.servezoneInterfaces.data.ICloudlyConfig>;
|
public appData!: plugins.smartconfig.AppData<plugins.servezoneInterfaces.data.ICloudlyConfig>;
|
||||||
public data: plugins.servezoneInterfaces.data.ICloudlyConfig;
|
public data!: plugins.servezoneInterfaces.data.ICloudlyConfig;
|
||||||
|
|
||||||
constructor(cloudlyRefArg: Cloudly) {
|
constructor(cloudlyRefArg: Cloudly) {
|
||||||
this.cloudlyRef = cloudlyRefArg;
|
this.cloudlyRef = cloudlyRefArg;
|
||||||
@@ -17,12 +17,12 @@ export class CloudlyConfig {
|
|||||||
|
|
||||||
public async init(configArg?: plugins.servezoneInterfaces.data.ICloudlyConfig) {
|
public async init(configArg?: plugins.servezoneInterfaces.data.ICloudlyConfig) {
|
||||||
this.appData =
|
this.appData =
|
||||||
await plugins.npmextra.AppData.createAndInit<plugins.servezoneInterfaces.data.ICloudlyConfig>(
|
await plugins.smartconfig.AppData.createAndInit<plugins.servezoneInterfaces.data.ICloudlyConfig>(
|
||||||
{
|
{
|
||||||
envMapping: {
|
envMapping: {
|
||||||
environment: 'SERVEZONE_ENVIRONMENT' as 'production' | 'integration',
|
environment: 'SERVEZONE_ENVIRONMENT' as 'production' | 'integration',
|
||||||
letsEncryptEmail: 'hard:domains@lossless.org',
|
letsEncryptEmail: 'hard:domains@lossless.org',
|
||||||
letsEncryptPrivateKey: null,
|
letsEncryptPrivateKey: undefined,
|
||||||
publicUrl: 'SERVEZONE_URL',
|
publicUrl: 'SERVEZONE_URL',
|
||||||
publicPort: 'SERVEZONE_PORT',
|
publicPort: 'SERVEZONE_PORT',
|
||||||
mongoDescriptor: {
|
mongoDescriptor: {
|
||||||
@@ -50,6 +50,7 @@ export class CloudlyConfig {
|
|||||||
'sslMode',
|
'sslMode',
|
||||||
'environment',
|
'environment',
|
||||||
'mongoDescriptor',
|
'mongoDescriptor',
|
||||||
|
's3Descriptor',
|
||||||
],
|
],
|
||||||
overwriteObject: configArg,
|
overwriteObject: configArg,
|
||||||
},
|
},
|
||||||
|
|||||||
+30
-48
@@ -11,13 +11,13 @@ export class CloudlyServer {
|
|||||||
* a reference to the cloudly instance
|
* a reference to the cloudly instance
|
||||||
*/
|
*/
|
||||||
public cloudlyRef: Cloudly;
|
public cloudlyRef: Cloudly;
|
||||||
public additionalHandlers: plugins.typedserver.servertools.Handler[] = [];
|
public additionalHandlers: plugins.typedserver.IRouteHandler[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the smartexpress server handling the actual requests
|
* the smartexpress server handling the actual requests
|
||||||
*/
|
*/
|
||||||
public typedServer: plugins.typedserver.TypedServer;
|
public typedServer!: plugins.typedserver.TypedServer;
|
||||||
public typedsocketServer: plugins.typedsocket.TypedSocket;
|
public typedsocketServer!: plugins.typedsocket.TypedSocket;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* typedrouter
|
* typedrouter
|
||||||
@@ -39,13 +39,13 @@ export class CloudlyServer {
|
|||||||
*/
|
*/
|
||||||
public async start() {
|
public async start() {
|
||||||
logger.log('info', `cloudly domain is ${this.cloudlyRef.config.data.publicUrl}`);
|
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') {
|
if (this.cloudlyRef.config.data.sslMode === 'letsencrypt') {
|
||||||
logger.log('info', `Using letsencrypt for ssl mode. Trying to obtain a certificate...`);
|
logger.log('info', `Using letsencrypt for ssl mode. Trying to obtain a certificate...`);
|
||||||
logger.log('info', `This might take 10 minutes...`);
|
logger.log('info', `This might take 10 minutes...`);
|
||||||
sslCert = await this.cloudlyRef.letsencryptConnector.getCertificateForDomain(
|
sslCert = await this.cloudlyRef.letsencryptConnector.getCertificateForDomain(
|
||||||
this.cloudlyRef.config.data.publicUrl,
|
this.cloudlyRef.config.data.publicUrl!,
|
||||||
);
|
);
|
||||||
logger.log(
|
logger.log(
|
||||||
'success',
|
'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
|
// server
|
||||||
this.typedServer = new plugins.typedserver.TypedServer({
|
this.typedServer = new plugins.typedserver.TypedServer({
|
||||||
cors: true,
|
cors: true,
|
||||||
@@ -89,43 +72,42 @@ export class CloudlyServer {
|
|||||||
injectReload: true,
|
injectReload: true,
|
||||||
serveDir: paths.distServeDir,
|
serveDir: paths.distServeDir,
|
||||||
watch: true,
|
watch: true,
|
||||||
enableCompression: true,
|
compression: {
|
||||||
preferredCompressionMethod: 'gzip',
|
enabled: true,
|
||||||
|
algorithms: ['gzip'],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
this.typedsocketServer = this.typedServer.typedsocket;
|
||||||
this.typedServer.typedrouter.addTypedRouter(this.typedrouter);
|
this.typedServer.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
this.typedServer.server.addRoute(
|
this.typedServer.addRoute(
|
||||||
'/v2',
|
'/v2',
|
||||||
new plugins.typedserver.servertools.Handler('ALL', async (req, res) => {
|
'ALL',
|
||||||
await this.cloudlyRef.registryManager.handleHttpRequest(req, res);
|
async (ctx) => this.cloudlyRef.registryManager.handleHttpRequest(ctx),
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
this.typedServer.server.addRoute(
|
this.typedServer.addRoute(
|
||||||
'/v2/{*splat}',
|
'/v2/*',
|
||||||
new plugins.typedserver.servertools.Handler('ALL', async (req, res) => {
|
'ALL',
|
||||||
await this.cloudlyRef.registryManager.handleHttpRequest(req, res);
|
async (ctx) => this.cloudlyRef.registryManager.handleHttpRequest(ctx),
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
this.typedServer.server.addRoute(
|
this.typedServer.addRoute(
|
||||||
'/curlfresh/:scriptname',
|
'/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',
|
'/baseos/v1/nodes/register',
|
||||||
new plugins.typedserver.servertools.Handler('POST', async (req, res) => {
|
'POST',
|
||||||
await this.cloudlyRef.baseOsManager.handleRegisterHttpRequest(req, res);
|
async (ctx) => this.cloudlyRef.baseOsManager.handleRegisterHttpRequest(ctx),
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
this.typedServer.server.addRoute(
|
this.typedServer.addRoute(
|
||||||
'/baseos/v1/nodes/heartbeat',
|
'/baseos/v1/nodes/heartbeat',
|
||||||
new plugins.typedserver.servertools.Handler('POST', async (req, res) => {
|
'POST',
|
||||||
await this.cloudlyRef.baseOsManager.handleHeartbeatHttpRequest(req, res);
|
async (ctx) => this.cloudlyRef.baseOsManager.handleHeartbeatHttpRequest(ctx),
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
this.typedServer.server.addRoute(
|
this.typedServer.addRoute(
|
||||||
'/baseos/v1/images/:buildId/download',
|
'/baseos/v1/images/:buildId/download',
|
||||||
new plugins.typedserver.servertools.Handler('GET', async (req, res) => {
|
'GET',
|
||||||
await this.cloudlyRef.baseOsManager.handleImageDownloadHttpRequest(req, res);
|
async (ctx) => this.cloudlyRef.baseOsManager.handleImageDownloadHttpRequest(ctx),
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
await this.typedServer.start();
|
await this.typedServer.start();
|
||||||
}
|
}
|
||||||
@@ -134,6 +116,6 @@ export class CloudlyServer {
|
|||||||
* stop the reception instance
|
* stop the reception instance
|
||||||
*/
|
*/
|
||||||
public async stop() {
|
public async stop() {
|
||||||
await this.typedServer.stop();
|
await this.typedServer?.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Cloudly } from '../classes.cloudly.js';
|
|||||||
*/
|
*/
|
||||||
export class CloudflareConnector {
|
export class CloudflareConnector {
|
||||||
private cloudlyRef: Cloudly;
|
private cloudlyRef: Cloudly;
|
||||||
public cloudflare: plugins.cloudflare.CloudflareAccount;
|
public cloudflare?: plugins.cloudflare.CloudflareAccount;
|
||||||
|
|
||||||
constructor(cloudlyArg: Cloudly) {
|
constructor(cloudlyArg: Cloudly) {
|
||||||
this.cloudlyRef = cloudlyArg;
|
this.cloudlyRef = cloudlyArg;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Cloudly } from '../classes.cloudly.js';
|
|||||||
|
|
||||||
export class LetsencryptConnector {
|
export class LetsencryptConnector {
|
||||||
private cloudlyRef: Cloudly;
|
private cloudlyRef: Cloudly;
|
||||||
private smartacme: plugins.smartacme.SmartAcme;
|
private smartacme!: plugins.smartacme.SmartAcme;
|
||||||
|
|
||||||
constructor(cloudlyArg: Cloudly) {
|
constructor(cloudlyArg: Cloudly) {
|
||||||
this.cloudlyRef = cloudlyArg;
|
this.cloudlyRef = cloudlyArg;
|
||||||
@@ -18,6 +18,10 @@ export class LetsencryptConnector {
|
|||||||
* inits letsencrypt
|
* inits letsencrypt
|
||||||
*/
|
*/
|
||||||
public async init() {
|
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
|
// Create DNS-01 challenge handler using Cloudflare
|
||||||
const dnsHandler = new plugins.smartacme.handlers.Dns01Handler(
|
const dnsHandler = new plugins.smartacme.handlers.Dns01Handler(
|
||||||
this.cloudlyRef.cloudflareConnector.cloudflare
|
this.cloudlyRef.cloudflareConnector.cloudflare
|
||||||
@@ -25,13 +29,13 @@ export class LetsencryptConnector {
|
|||||||
|
|
||||||
// Create MongoDB certificate manager
|
// Create MongoDB certificate manager
|
||||||
const certManager = new plugins.smartacme.certmanagers.MongoCertManager(
|
const certManager = new plugins.smartacme.certmanagers.MongoCertManager(
|
||||||
this.cloudlyRef.config.data.mongoDescriptor
|
this.cloudlyRef.config.data.mongoDescriptor!
|
||||||
);
|
);
|
||||||
|
|
||||||
this.smartacme = new plugins.smartacme.SmartAcme({
|
this.smartacme = new plugins.smartacme.SmartAcme({
|
||||||
accountEmail: this.cloudlyRef.config.data.letsEncryptEmail,
|
accountEmail: this.cloudlyRef.config.data.letsEncryptEmail!,
|
||||||
accountPrivateKey: this.cloudlyRef.config.data.letsEncryptPrivateKey,
|
accountPrivateKey: this.cloudlyRef.config.data.letsEncryptPrivateKey,
|
||||||
environment: this.cloudlyRef.config.data.environment,
|
environment: this.cloudlyRef.config.data.environment!,
|
||||||
certManager: certManager,
|
certManager: certManager,
|
||||||
challengeHandlers: [dnsHandler],
|
challengeHandlers: [dnsHandler],
|
||||||
});
|
});
|
||||||
@@ -45,6 +49,6 @@ export class LetsencryptConnector {
|
|||||||
* stops the instance
|
* stops the instance
|
||||||
*/
|
*/
|
||||||
public async stop() {
|
public async stop() {
|
||||||
await this.smartacme.stop();
|
await this.smartacme?.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Cloudly } from '../classes.cloudly.js';
|
|||||||
export class MongodbConnector {
|
export class MongodbConnector {
|
||||||
// INSTANCE
|
// INSTANCE
|
||||||
private cloudlyRef: Cloudly;
|
private cloudlyRef: Cloudly;
|
||||||
public smartdataDb: plugins.smartdata.SmartdataDb;
|
public smartdataDb!: plugins.smartdata.SmartdataDb;
|
||||||
|
|
||||||
constructor(cloudlyRefArg: Cloudly) {
|
constructor(cloudlyRefArg: Cloudly) {
|
||||||
this.cloudlyRef = cloudlyRefArg;
|
this.cloudlyRef = cloudlyRefArg;
|
||||||
@@ -12,7 +12,7 @@ export class MongodbConnector {
|
|||||||
|
|
||||||
public async init() {
|
public async init() {
|
||||||
this.smartdataDb = new plugins.smartdata.SmartdataDb(
|
this.smartdataDb = new plugins.smartdata.SmartdataDb(
|
||||||
this.cloudlyRef.config.data.mongoDescriptor,
|
this.cloudlyRef.config.data.mongoDescriptor!,
|
||||||
);
|
);
|
||||||
await this.smartdataDb.init();
|
await this.smartdataDb.init();
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-6
@@ -3,12 +3,12 @@ import * as paths from './paths.js';
|
|||||||
|
|
||||||
export const logger = new plugins.smartlog.Smartlog({
|
export const logger = new plugins.smartlog.Smartlog({
|
||||||
logContext: {
|
logContext: {
|
||||||
company: null,
|
company: undefined,
|
||||||
environment: null,
|
environment: undefined,
|
||||||
runtime: null,
|
runtime: undefined,
|
||||||
zone: null,
|
zone: undefined,
|
||||||
companyunit: null,
|
companyunit: undefined,
|
||||||
containerName: null,
|
containerName: undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
logger.enableConsole({
|
logger.enableConsole({
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export class CloudlyAuthManager {
|
|||||||
public CAuthorization = plugins.smartdata.setDefaultManagerForDoc(this, Authorization);
|
public CAuthorization = plugins.smartdata.setDefaultManagerForDoc(this, Authorization);
|
||||||
|
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
public smartjwtInstance: plugins.smartjwt.SmartJwt<IJwtData>;
|
public smartjwtInstance!: plugins.smartjwt.SmartJwt<IJwtData>;
|
||||||
|
|
||||||
constructor(cloudlyRef: Cloudly) {
|
constructor(cloudlyRef: Cloudly) {
|
||||||
this.cloudlyRef = cloudlyRef;
|
this.cloudlyRef = cloudlyRef;
|
||||||
@@ -73,7 +73,7 @@ export class CloudlyAuthManager {
|
|||||||
identity: {
|
identity: {
|
||||||
jwt,
|
jwt,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
name: user.data.username,
|
name: user.data.username || user.id,
|
||||||
expiresAt: expiresAtTimestamp,
|
expiresAt: expiresAtTimestamp,
|
||||||
role: user.data.role,
|
role: user.data.role,
|
||||||
type: user.data.type,
|
type: user.data.type,
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ export class User extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
|
|
||||||
// INSTANCE
|
// INSTANCE
|
||||||
@plugins.smartdata.unI()
|
@plugins.smartdata.unI()
|
||||||
public id: string;
|
public id!: string;
|
||||||
|
|
||||||
@plugins.smartdata.svDb()
|
@plugins.smartdata.svDb()
|
||||||
public data: plugins.servezoneInterfaces.data.IUser['data'];
|
public data!: plugins.servezoneInterfaces.data.IUser['data'];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -256,17 +256,19 @@ export class CloudlyBackupManager {
|
|||||||
backup.tags = requestArg.tags;
|
backup.tags = requestArg.tags;
|
||||||
await backup.save();
|
await backup.save();
|
||||||
|
|
||||||
|
const replicationEnabled = (requestArg as any).replicate !== false && !!process.env.CLOUDLY_BACKUP_TARGET_TYPE;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await this.fireCoreflowRequest('executeServiceBackup', {
|
const result = await this.fireCoreflowRequest('executeServiceBackup', {
|
||||||
backupId: backup.id,
|
backupId: backup.id,
|
||||||
service: await service.createSavableObject(),
|
service: await service.createSavableObject(),
|
||||||
tags: requestArg.tags,
|
tags: requestArg.tags,
|
||||||
replication: {
|
replication: {
|
||||||
enabled: true,
|
enabled: replicationEnabled,
|
||||||
},
|
},
|
||||||
}, backup.clusterId);
|
}, backup.clusterId);
|
||||||
backup.snapshots = result.snapshots || [];
|
backup.snapshots = result.snapshots || [];
|
||||||
if (!result.replication) {
|
if (replicationEnabled && !result.replication) {
|
||||||
throw new Error('Coreflow did not complete remote backup replication');
|
throw new Error('Coreflow did not complete remote backup replication');
|
||||||
}
|
}
|
||||||
backup.replication = result.replication;
|
backup.replication = result.replication;
|
||||||
|
|||||||
@@ -67,7 +67,10 @@ class S3BackupTargetWriter implements IBackupTargetWriter {
|
|||||||
: {}),
|
: {}),
|
||||||
} as any);
|
} as any);
|
||||||
const bucketName = requiredEnv('CLOUDLY_BACKUP_S3_BUCKET');
|
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;
|
return await this.bucketPromise;
|
||||||
|
|||||||
@@ -12,29 +12,38 @@ export class BareMetal extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
public static async createFromHetznerServer(
|
public static async createFromHetznerServer(
|
||||||
hetznerServerArg: plugins.hetznercloud.HetznerServer,
|
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();
|
const newBareMetal = new BareMetal();
|
||||||
newBareMetal.id = plugins.smartunique.shortId(8);
|
newBareMetal.id = plugins.smartunique.shortId(8);
|
||||||
const data: plugins.servezoneInterfaces.data.IBareMetal['data'] = {
|
const data: plugins.servezoneInterfaces.data.IBareMetal['data'] = {
|
||||||
hostname: hetznerServerArg.data.name,
|
hostname: serverData.name,
|
||||||
primaryIp: hetznerServerArg.data.public_net.ipv4.ip,
|
primaryIp: ipv4.ip,
|
||||||
provider: 'hetzner',
|
provider: 'hetzner',
|
||||||
location: hetznerServerArg.data.datacenter.name,
|
location: serverData.datacenter.name,
|
||||||
specs: {
|
specs: {
|
||||||
cpuModel: hetznerServerArg.data.server_type.cpu_type,
|
cpuModel: serverData.server_type.cpu_type,
|
||||||
cpuCores: hetznerServerArg.data.server_type.cores,
|
cpuCores: serverData.server_type.cores,
|
||||||
memoryGB: hetznerServerArg.data.server_type.memory,
|
memoryGB: serverData.server_type.memory,
|
||||||
storageGB: hetznerServerArg.data.server_type.disk,
|
storageGB: serverData.server_type.disk,
|
||||||
storageType: 'nvme',
|
storageType: 'nvme',
|
||||||
},
|
},
|
||||||
powerState: hetznerServerArg.data.status === 'running' ? 'on' : 'off',
|
powerState: serverData.status === 'running' ? 'on' : 'off',
|
||||||
osInfo: {
|
osInfo: {
|
||||||
name: 'Debian',
|
name: 'Debian',
|
||||||
version: '12',
|
version: '12',
|
||||||
},
|
},
|
||||||
assignedNodeIds: [],
|
assignedNodeIds: [],
|
||||||
providerMetadata: {
|
providerMetadata: {
|
||||||
hetznerServerId: hetznerServerArg.data.id,
|
hetznerServerId: serverData.id,
|
||||||
hetznerServerName: hetznerServerArg.data.name,
|
hetznerServerName: serverData.name,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
Object.assign(newBareMetal, { data });
|
Object.assign(newBareMetal, { data });
|
||||||
@@ -44,10 +53,10 @@ export class BareMetal extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
|
|
||||||
// INSTANCE
|
// INSTANCE
|
||||||
@plugins.smartdata.unI()
|
@plugins.smartdata.unI()
|
||||||
public id: string;
|
public id!: string;
|
||||||
|
|
||||||
@plugins.smartdata.svDb()
|
@plugins.smartdata.svDb()
|
||||||
public data: plugins.servezoneInterfaces.data.IBareMetal['data'];
|
public data!: plugins.servezoneInterfaces.data.IBareMetal['data'];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export class CloudlyBaremetalManager {
|
|||||||
public cloudlyRef: Cloudly;
|
public cloudlyRef: Cloudly;
|
||||||
public typedRouter = new plugins.typedrequest.TypedRouter();
|
public typedRouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
|
||||||
public hetznerAccount: plugins.hetznercloud.HetznerAccount;
|
public hetznerAccount?: plugins.hetznercloud.HetznerAccount;
|
||||||
|
|
||||||
public get db() {
|
public get db() {
|
||||||
return this.cloudlyRef.mongodbConnector.smartdataDb;
|
return this.cloudlyRef.mongodbConnector.smartdataDb;
|
||||||
@@ -119,18 +119,22 @@ export class CloudlyBaremetalManager {
|
|||||||
* Create baremetal from Hetzner server
|
* Create baremetal from Hetzner server
|
||||||
*/
|
*/
|
||||||
public async createBaremetalFromHetznerServer(hetznerServer: plugins.hetznercloud.HetznerServer): Promise<BareMetal> {
|
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
|
// Check if baremetal already exists for this Hetzner server
|
||||||
const existingBaremetals = await this.CBareMetal.getInstances({});
|
const existingBaremetals = await this.CBareMetal.getInstances({});
|
||||||
for (const baremetal of existingBaremetals) {
|
for (const baremetal of existingBaremetals) {
|
||||||
if (baremetal.data.providerMetadata?.hetznerServerId === hetznerServer.data.id) {
|
if (baremetal.data.providerMetadata?.hetznerServerId === serverData.id) {
|
||||||
logger.log('info', `BareMetal already exists for Hetzner server ${hetznerServer.data.id}`);
|
logger.log('info', `BareMetal already exists for Hetzner server ${serverData.id}`);
|
||||||
return baremetal;
|
return baremetal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new baremetal
|
// Create new baremetal
|
||||||
const newBaremetal = await BareMetal.createFromHetznerServer(hetznerServer);
|
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;
|
return newBaremetal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -305,15 +305,14 @@ export class CloudlyBaseOsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async handleRegisterHttpRequest(
|
public async handleRegisterHttpRequest(
|
||||||
reqArg: plugins.typedserver.Request,
|
ctxArg: plugins.typedserver.IRequestContext,
|
||||||
resArg: plugins.typedserver.Response,
|
): Promise<Response> {
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
const requestData = await this.readJsonBody<IBaseOsRegisterRequest>(reqArg);
|
const requestData = await this.readJsonBody<IBaseOsRegisterRequest>(ctxArg);
|
||||||
const response = await this.registerNode(requestData);
|
const response = await this.registerNode(requestData);
|
||||||
this.sendJson(resArg, 200, response);
|
return this.createJsonResponse(200, response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.sendJson(resArg, 400, {
|
return this.createJsonResponse(400, {
|
||||||
accepted: false,
|
accepted: false,
|
||||||
message: `BaseOS registration failed: ${(error as Error).message}`,
|
message: `BaseOS registration failed: ${(error as Error).message}`,
|
||||||
} satisfies IBaseOsRegisterResponse);
|
} satisfies IBaseOsRegisterResponse);
|
||||||
@@ -321,15 +320,14 @@ export class CloudlyBaseOsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async handleHeartbeatHttpRequest(
|
public async handleHeartbeatHttpRequest(
|
||||||
reqArg: plugins.typedserver.Request,
|
ctxArg: plugins.typedserver.IRequestContext,
|
||||||
resArg: plugins.typedserver.Response,
|
): Promise<Response> {
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
const requestData = await this.readJsonBody<IBaseOsHeartbeatRequest>(reqArg);
|
const requestData = await this.readJsonBody<IBaseOsHeartbeatRequest>(ctxArg);
|
||||||
const response = await this.acceptHeartbeat(requestData);
|
const response = await this.acceptHeartbeat(requestData);
|
||||||
this.sendJson(resArg, 200, response);
|
return this.createJsonResponse(200, response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.sendJson(resArg, 400, {
|
return this.createJsonResponse(400, {
|
||||||
accepted: false,
|
accepted: false,
|
||||||
message: `BaseOS heartbeat failed: ${(error as Error).message}`,
|
message: `BaseOS heartbeat failed: ${(error as Error).message}`,
|
||||||
} satisfies IBaseOsHeartbeatResponse);
|
} satisfies IBaseOsHeartbeatResponse);
|
||||||
@@ -337,37 +335,35 @@ export class CloudlyBaseOsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async handleImageDownloadHttpRequest(
|
public async handleImageDownloadHttpRequest(
|
||||||
reqArg: plugins.typedserver.Request,
|
ctxArg: plugins.typedserver.IRequestContext,
|
||||||
resArg: plugins.typedserver.Response,
|
): Promise<Response> {
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
const requestUrl = new URL((reqArg as any).originalUrl || reqArg.url || '/', 'http://localhost');
|
const buildId = ctxArg.params.buildId || ctxArg.url.pathname.split('/').at(-2);
|
||||||
const buildId = requestUrl.pathname.split('/').at(-2);
|
const token = ctxArg.url.searchParams.get('token');
|
||||||
const token = requestUrl.searchParams.get('token');
|
|
||||||
if (!buildId || !token) {
|
if (!buildId || !token) {
|
||||||
this.sendJson(resArg, 400, { errorText: 'build id or download token missing' });
|
return this.createJsonResponse(400, { errorText: 'build id or download token missing' });
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const build = await this.getImageBuildById(buildId);
|
const build = await this.getImageBuildById(buildId);
|
||||||
if (build.downloadTokenHash !== this.hashSecret(token) || (build.downloadTokenExpiresAt || 0) < Date.now()) {
|
if (build.downloadTokenHash !== this.hashSecret(token) || (build.downloadTokenExpiresAt || 0) < Date.now()) {
|
||||||
this.sendJson(resArg, 403, { errorText: 'download token is invalid or expired' });
|
return this.createJsonResponse(403, { errorText: 'download token is invalid or expired' });
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (build.data.status !== 'ready' || !build.data.artifact) {
|
if (build.data.status !== 'ready' || !build.data.artifact) {
|
||||||
this.sendJson(resArg, 409, { errorText: 'image build is not ready' });
|
return this.createJsonResponse(409, { errorText: 'image build is not ready' });
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const artifact = build.data.artifact;
|
const artifact = build.data.artifact;
|
||||||
const bucket = await this.getArtifactBucket(artifact.bucketName);
|
const bucket = await this.getArtifactBucket(artifact.bucketName);
|
||||||
const artifactStream = await bucket.fastGetStream({ path: artifact.key }, 'nodestream');
|
const artifactStream = await bucket.fastGetStream({ path: artifact.key }, 'nodestream');
|
||||||
resArg.status(200);
|
return new Response(nodeStream.Readable.toWeb(artifactStream as nodeStream.Readable) as ReadableStream, {
|
||||||
resArg.setHeader('Content-Type', artifact.contentType || 'application/octet-stream');
|
status: 200,
|
||||||
resArg.setHeader('Content-Length', String(artifact.size));
|
headers: {
|
||||||
resArg.setHeader('Content-Disposition', `attachment; filename="${artifact.filename}"`);
|
'Content-Type': artifact.contentType || 'application/octet-stream',
|
||||||
(artifactStream as nodeStream.Readable).pipe(resArg as any);
|
'Content-Length': String(artifact.size),
|
||||||
|
'Content-Disposition': `attachment; filename="${artifact.filename}"`,
|
||||||
|
},
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.sendJson(resArg, 500, {
|
return this.createJsonResponse(500, {
|
||||||
errorText: `BaseOS image download failed: ${(error as Error).message}`,
|
errorText: `BaseOS image download failed: ${(error as Error).message}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -986,22 +982,20 @@ export class CloudlyBaseOsManager {
|
|||||||
&& typeof runtimeInfo.checkedAt === 'number';
|
&& typeof runtimeInfo.checkedAt === 'number';
|
||||||
}
|
}
|
||||||
|
|
||||||
private async readJsonBody<T>(reqArg: plugins.typedserver.Request): Promise<T> {
|
private async readJsonBody<T>(ctxArg: plugins.typedserver.IRequestContext): Promise<T> {
|
||||||
const chunks: Buffer[] = [];
|
const bodyString = (await ctxArg.text()).trim();
|
||||||
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();
|
|
||||||
return bodyString ? JSON.parse(bodyString) as T : {} as T;
|
return bodyString ? JSON.parse(bodyString) as T : {} as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendJson(
|
private createJsonResponse(
|
||||||
resArg: plugins.typedserver.Response,
|
|
||||||
statusCodeArg: number,
|
statusCodeArg: number,
|
||||||
bodyArg: object,
|
bodyArg: object,
|
||||||
) {
|
): Response {
|
||||||
resArg.status(statusCodeArg);
|
return new Response(JSON.stringify(bodyArg), {
|
||||||
resArg.setHeader('Content-Type', 'application/json');
|
status: statusCodeArg,
|
||||||
resArg.end(JSON.stringify(bodyArg));
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ export class Cluster extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
|
|
||||||
// INSTANCE
|
// INSTANCE
|
||||||
@plugins.smartdata.unI()
|
@plugins.smartdata.unI()
|
||||||
public id: string;
|
public id!: string;
|
||||||
|
|
||||||
@plugins.smartdata.svDb()
|
@plugins.smartdata.svDb()
|
||||||
public data: plugins.servezoneInterfaces.data.ICluster['data'];
|
public data!: plugins.servezoneInterfaces.data.ICluster['data'];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|||||||
@@ -22,16 +22,19 @@ export class ClusterManager {
|
|||||||
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
|
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
|
|
||||||
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.cluster.IRequest_CreateCluster>(
|
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.cluster.IRequest_CreateCluster>(
|
||||||
new plugins.typedrequest.TypedHandler('createCluster', async (dataArg) => {
|
new plugins.typedrequest.TypedHandler('createCluster', async (dataArg, toolsArg) => {
|
||||||
// TODO: guards
|
await toolsArg!.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], dataArg);
|
||||||
const setupMode = dataArg.setupMode || 'manual'; // Default to manual if not specified
|
const setupMode = dataArg.setupMode || 'manual'; // Default to manual if not specified
|
||||||
const cluster = await this.createCluster({
|
const cluster = await this.createCluster({
|
||||||
id: plugins.smartunique.uniSimple('cluster'),
|
id: plugins.smartunique.uniSimple('cluster'),
|
||||||
data: {
|
data: {
|
||||||
userId: null, // this is created by the createCluster method
|
userId: '', // this is created by the createCluster method
|
||||||
name: dataArg.clusterName,
|
name: dataArg.clusterName,
|
||||||
setupMode: setupMode,
|
setupMode: setupMode,
|
||||||
acmeInfo: null,
|
acmeInfo: {
|
||||||
|
serverAddress: '',
|
||||||
|
serverSecret: '',
|
||||||
|
},
|
||||||
cloudlyUrl: `https://${this.cloudlyRef.config.data.publicUrl}:${this.cloudlyRef.config.data.publicPort}/`,
|
cloudlyUrl: `https://${this.cloudlyRef.config.data.publicUrl}:${this.cloudlyRef.config.data.publicPort}/`,
|
||||||
nodes: [],
|
nodes: [],
|
||||||
sshKeys: [],
|
sshKeys: [],
|
||||||
@@ -51,8 +54,8 @@ export class ClusterManager {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.cluster.IReq_Any_Cloudly_GetClusters>(
|
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.cluster.IReq_Any_Cloudly_GetClusters>(
|
||||||
new plugins.typedrequest.TypedHandler('getClusters', async (dataArg) => {
|
new plugins.typedrequest.TypedHandler('getClusters', async (dataArg, toolsArg) => {
|
||||||
// TODO: do authentication here
|
await toolsArg!.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], dataArg);
|
||||||
const clusters = await this.getAllClusters();
|
const clusters = await this.getAllClusters();
|
||||||
return {
|
return {
|
||||||
clusters: await Promise.all(
|
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
|
// delete cluster
|
||||||
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.cluster.IReq_Any_Cloudly_DeleteClusterById>(
|
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.cluster.IReq_Any_Cloudly_DeleteClusterById>(
|
||||||
new plugins.typedrequest.TypedHandler('deleteClusterById', async (reqDataArg, toolsArg) => {
|
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);
|
await this.deleteCluster(reqDataArg.clusterId);
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -134,7 +168,6 @@ export class ClusterManager {
|
|||||||
* @param configObjectArg
|
* @param configObjectArg
|
||||||
*/
|
*/
|
||||||
public async createCluster(configObjectArg: plugins.servezoneInterfaces.data.ICluster) {
|
public async createCluster(configObjectArg: plugins.servezoneInterfaces.data.ICluster) {
|
||||||
// TODO: guards
|
|
||||||
// lets create the cluster user
|
// lets create the cluster user
|
||||||
const clusterUser = new this.cloudlyRef.authManager.CUser({
|
const clusterUser = new this.cloudlyRef.authManager.CUser({
|
||||||
id: await this.cloudlyRef.authManager.CUser.getNewId(),
|
id: await this.cloudlyRef.authManager.CUser.getNewId(),
|
||||||
|
|||||||
@@ -9,28 +9,28 @@ export class Deployment extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
public id: string = plugins.smartunique.uniSimple('deployment');
|
public id: string = plugins.smartunique.uniSimple('deployment');
|
||||||
|
|
||||||
@plugins.smartdata.svDb()
|
@plugins.smartdata.svDb()
|
||||||
public serviceId: string;
|
public serviceId!: string;
|
||||||
|
|
||||||
@plugins.smartdata.svDb()
|
@plugins.smartdata.svDb()
|
||||||
public nodeId: string;
|
public nodeId!: string;
|
||||||
|
|
||||||
@plugins.smartdata.svDb()
|
@plugins.smartdata.svDb()
|
||||||
public containerId?: string;
|
public containerId?: string;
|
||||||
|
|
||||||
@plugins.smartdata.svDb()
|
@plugins.smartdata.svDb()
|
||||||
public usedImageId: string;
|
public usedImageId!: string;
|
||||||
|
|
||||||
@plugins.smartdata.svDb()
|
@plugins.smartdata.svDb()
|
||||||
public version: string;
|
public version!: string;
|
||||||
|
|
||||||
@plugins.smartdata.svDb()
|
@plugins.smartdata.svDb()
|
||||||
public deployedAt: number;
|
public deployedAt!: number;
|
||||||
|
|
||||||
@plugins.smartdata.svDb()
|
@plugins.smartdata.svDb()
|
||||||
public deploymentLog: string[] = [];
|
public deploymentLog: string[] = [];
|
||||||
|
|
||||||
@plugins.smartdata.svDb()
|
@plugins.smartdata.svDb()
|
||||||
public status: 'scheduled' | 'starting' | 'running' | 'stopping' | 'stopped' | 'failed';
|
public status!: 'scheduled' | 'starting' | 'running' | 'stopping' | 'stopped' | 'failed';
|
||||||
|
|
||||||
@plugins.smartdata.svDb()
|
@plugins.smartdata.svDb()
|
||||||
public healthStatus?: 'healthy' | 'unhealthy' | 'unknown';
|
public healthStatus?: 'healthy' | 'unhealthy' | 'unknown';
|
||||||
|
|||||||
@@ -79,10 +79,10 @@ export class DnsEntry extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
|
|
||||||
// INSTANCE
|
// INSTANCE
|
||||||
@plugins.smartdata.svDb()
|
@plugins.smartdata.svDb()
|
||||||
public id: string;
|
public id!: string;
|
||||||
|
|
||||||
@plugins.smartdata.svDb()
|
@plugins.smartdata.svDb()
|
||||||
public data: plugins.servezoneInterfaces.data.IDnsEntry['data'];
|
public data!: plugins.servezoneInterfaces.data.IDnsEntry['data'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates the DNS entry data
|
* Validates the DNS entry data
|
||||||
|
|||||||
@@ -95,10 +95,10 @@ export class Domain extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
|
|
||||||
// INSTANCE
|
// INSTANCE
|
||||||
@plugins.smartdata.unI()
|
@plugins.smartdata.unI()
|
||||||
public id: string;
|
public id!: string;
|
||||||
|
|
||||||
@plugins.smartdata.svDb()
|
@plugins.smartdata.svDb()
|
||||||
public data: plugins.servezoneInterfaces.data.IDomain['data'];
|
public data!: plugins.servezoneInterfaces.data.IDomain['data'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify domain ownership
|
* Verify domain ownership
|
||||||
|
|||||||
@@ -107,10 +107,10 @@ export class ExternalRegistry extends plugins.smartdata.SmartDataDbDoc<ExternalR
|
|||||||
// INSTANCE
|
// INSTANCE
|
||||||
|
|
||||||
@plugins.smartdata.svDb()
|
@plugins.smartdata.svDb()
|
||||||
public id: string;
|
public id!: string;
|
||||||
|
|
||||||
@plugins.smartdata.svDb()
|
@plugins.smartdata.svDb()
|
||||||
public data: plugins.servezoneInterfaces.data.IExternalRegistry['data'];
|
public data!: plugins.servezoneInterfaces.data.IExternalRegistry['data'];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -170,10 +170,11 @@ export class ExternalRegistry extends plugins.smartdata.SmartDataDbDoc<ExternalR
|
|||||||
|
|
||||||
return { success: false, message: 'Unknown registry type' };
|
return { success: false, message: 'Unknown registry type' };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
this.data.status = 'error';
|
this.data.status = 'error';
|
||||||
this.data.lastError = error.message;
|
this.data.lastError = errorMessage;
|
||||||
await this.save();
|
await this.save();
|
||||||
return { success: false, message: error.message };
|
return { success: false, message: errorMessage };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ export class Image extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
}
|
}
|
||||||
|
|
||||||
@plugins.smartdata.unI()
|
@plugins.smartdata.unI()
|
||||||
public id: string;
|
public id!: string;
|
||||||
|
|
||||||
@plugins.smartdata.svDb()
|
@plugins.smartdata.svDb()
|
||||||
public data: plugins.servezoneInterfaces.data.IImage['data'];
|
public data!: plugins.servezoneInterfaces.data.IImage['data'];
|
||||||
|
|
||||||
public async getVersions() {}
|
public async getVersions() {}
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import { Image } from './classes.image.js';
|
|||||||
export class ImageManager {
|
export class ImageManager {
|
||||||
cloudlyRef: Cloudly;
|
cloudlyRef: Cloudly;
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
public smartbucketInstance: plugins.smartbucket.SmartBucket;
|
public smartbucketInstance!: plugins.smartbucket.SmartBucket;
|
||||||
public imageDir: plugins.smartbucket.Directory;
|
public imageDir!: plugins.smartbucket.Directory;
|
||||||
public dockerImageStore: plugins.docker.DockerImageStore;
|
public dockerImageStore!: plugins.docker.DockerImageStore;
|
||||||
|
|
||||||
get db() {
|
get db() {
|
||||||
return this.cloudlyRef.mongodbConnector.smartdataDb;
|
return this.cloudlyRef.mongodbConnector.smartdataDb;
|
||||||
@@ -26,7 +26,7 @@ export class ImageManager {
|
|||||||
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_CreateImage>(
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_CreateImage>(
|
||||||
'createImage',
|
'createImage',
|
||||||
async (reqArg, toolsArg) => {
|
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({
|
const image = await this.CImage.create({
|
||||||
name: reqArg.name,
|
name: reqArg.name,
|
||||||
description: reqArg.description,
|
description: reqArg.description,
|
||||||
@@ -41,7 +41,7 @@ export class ImageManager {
|
|||||||
|
|
||||||
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_GetImage>(
|
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_GetImage>(
|
||||||
new plugins.typedrequest.TypedHandler('getImage', async (reqArg, toolsArg) => {
|
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({
|
const image = await this.CImage.getInstance({
|
||||||
id: reqArg.imageId,
|
id: reqArg.imageId,
|
||||||
});
|
});
|
||||||
@@ -55,7 +55,7 @@ export class ImageManager {
|
|||||||
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_DeleteImage>(
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_DeleteImage>(
|
||||||
'deleteImage',
|
'deleteImage',
|
||||||
async (reqArg, toolsArg) => {
|
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({
|
const image = await this.CImage.getInstance({
|
||||||
id: reqArg.imageId,
|
id: reqArg.imageId,
|
||||||
});
|
});
|
||||||
@@ -69,7 +69,7 @@ export class ImageManager {
|
|||||||
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_GetAllImages>(
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.image.IRequest_GetAllImages>(
|
||||||
'getAllImages',
|
'getAllImages',
|
||||||
async (requestArg, toolsArg) => {
|
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({});
|
const images = await this.CImage.getInstances({});
|
||||||
return {
|
return {
|
||||||
images: await Promise.all(
|
images: await Promise.all(
|
||||||
@@ -96,9 +96,24 @@ export class ImageManager {
|
|||||||
throw new plugins.typedrequest.TypedResponseError('Image not found');
|
throw new plugins.typedrequest.TypedResponseError('Image not found');
|
||||||
}
|
}
|
||||||
const imageVersion = reqArg.versionString;
|
const imageVersion = reqArg.versionString;
|
||||||
|
if (!imageVersion) {
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('versionString is required');
|
||||||
|
}
|
||||||
console.log(
|
console.log(
|
||||||
`got request to push image version ${imageVersion} for image ${refImage.data.name}`,
|
`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;
|
const imagePushStream = reqArg.imageStream;
|
||||||
(async () => {
|
(async () => {
|
||||||
const smartWebDuplex = new plugins.smartstream.webstream.WebDuplexStream<
|
const smartWebDuplex = new plugins.smartstream.webstream.WebDuplexStream<
|
||||||
@@ -112,10 +127,12 @@ export class ImageManager {
|
|||||||
});
|
});
|
||||||
imagePushStream.writeToWebstream(smartWebDuplex.writable);
|
imagePushStream.writeToWebstream(smartWebDuplex.writable);
|
||||||
await this.dockerImageStore.storeImage(
|
await this.dockerImageStore.storeImage(
|
||||||
refImage.id,
|
storagePath,
|
||||||
plugins.smartstream.SmartDuplex.fromWebReadableStream(smartWebDuplex.readable),
|
plugins.smartstream.SmartDuplex.fromWebReadableStream(smartWebDuplex.readable),
|
||||||
);
|
);
|
||||||
})();
|
})().catch((error) => {
|
||||||
|
console.error(`failed to store image ${refImage.id}:${imageVersion}`, error);
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
allowed: true,
|
allowed: true,
|
||||||
};
|
};
|
||||||
@@ -133,13 +150,21 @@ export class ImageManager {
|
|||||||
const imageVersion = image.data.versions.find(
|
const imageVersion = image.data.versions.find(
|
||||||
(version) => version.versionString === reqArg.versionString,
|
(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',
|
'webstream',
|
||||||
);
|
);
|
||||||
const imageVirtualStream = new plugins.typedrequest.VirtualStream();
|
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 {
|
return {
|
||||||
imageStream: imageVirtualStream,
|
imageStream: imageVirtualStream,
|
||||||
};
|
};
|
||||||
@@ -150,21 +175,24 @@ export class ImageManager {
|
|||||||
|
|
||||||
public async start() {
|
public async start() {
|
||||||
// lets setup s3
|
// lets setup s3
|
||||||
const s3Descriptor: plugins.tsclass.storage.IS3Descriptor =
|
const s3Descriptor =
|
||||||
await this.cloudlyRef.config.appData.waitForAndGetKey('s3Descriptor');
|
await this.cloudlyRef.config.appData.waitForAndGetKey('s3Descriptor') as plugins.tsclass.storage.IS3Descriptor;
|
||||||
console.log(this.cloudlyRef.config.data.s3Descriptor);
|
console.log(this.cloudlyRef.config.data.s3Descriptor);
|
||||||
this.smartbucketInstance = new plugins.smartbucket.SmartBucket(
|
this.smartbucketInstance = new plugins.smartbucket.SmartBucket(
|
||||||
this.cloudlyRef.config.data.s3Descriptor,
|
this.cloudlyRef.config.data.s3Descriptor!,
|
||||||
);
|
);
|
||||||
const bucket = await this.smartbucketInstance.getBucketByName(s3Descriptor.bucketName);
|
const bucketName = s3Descriptor.bucketName!;
|
||||||
await bucket.fastPut({ path: 'images/00init', contents: 'init' });
|
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({
|
this.imageDir = await bucket.getDirectoryFromPath({
|
||||||
path: '/images',
|
path: '/images',
|
||||||
});
|
});
|
||||||
|
|
||||||
// lets setup dockerstore
|
// lets setup dockerstore
|
||||||
await plugins.smartfile.fs.ensureDir(paths.dockerImageStoreDir);
|
await plugins.fsPromises.mkdir(paths.dockerImageStoreDir, { recursive: true });
|
||||||
this.dockerImageStore = new plugins.docker.DockerImageStore({
|
this.dockerImageStore = new plugins.docker.DockerImageStore({
|
||||||
localDirPath: paths.dockerImageStoreDir,
|
localDirPath: paths.dockerImageStoreDir,
|
||||||
bucketDir: this.imageDir,
|
bucketDir: this.imageDir,
|
||||||
|
|||||||
@@ -34,10 +34,10 @@ export class ClusterNode extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
|
|
||||||
// INSTANCE
|
// INSTANCE
|
||||||
@plugins.smartdata.unI()
|
@plugins.smartdata.unI()
|
||||||
public id: string;
|
public id!: string;
|
||||||
|
|
||||||
@plugins.smartdata.svDb()
|
@plugins.smartdata.svDb()
|
||||||
public data: plugins.servezoneInterfaces.data.IClusterNode['data'];
|
public data!: plugins.servezoneInterfaces.data.IClusterNode['data'];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { logger } from '../logger.js';
|
import { logger } from '../logger.js';
|
||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import type { CloudlyNodeManager } from './classes.nodemanager.js';
|
import type { CloudlyNodeManager } from './classes.nodemanager.js';
|
||||||
|
import type { Cluster } from '../manager.cluster/classes.cluster.js';
|
||||||
|
|
||||||
export class CurlFresh {
|
export class CurlFresh {
|
||||||
public optionsArg = {
|
public optionsArg = {
|
||||||
@@ -39,33 +40,33 @@ bash -c "npm config set registry ${this.optionsArg.npmRegistry}"
|
|||||||
bash -c "pnpm install -g @serve.zone/spark"
|
bash -c "pnpm install -g @serve.zone/spark"
|
||||||
|
|
||||||
# lets install the spark daemon
|
# lets install the spark daemon
|
||||||
bash -c "spark installdaemon"
|
bash -c "spark installdaemon --mode=coreflow-node --cloudlyUrl='__CLOUDLY_URL__' --jumpcode='__JUMPCODE__'"
|
||||||
|
|
||||||
# TODO: start spark with jump code
|
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
|
|
||||||
public nodeManagerRef: CloudlyNodeManager;
|
public nodeManagerRef: CloudlyNodeManager;
|
||||||
public curlFreshRoute: plugins.typedserver.servertools.Route;
|
public async handleRequest(ctx: plugins.typedserver.IRequestContext): Promise<Response> {
|
||||||
public handler = new plugins.typedserver.servertools.Handler('ALL', async (req, res) => {
|
|
||||||
logger.log('info', 'curlfresh handler called. a server might be coming online soon :)');
|
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) {
|
switch (scriptname) {
|
||||||
case 'setup.sh':
|
case 'setup.sh':
|
||||||
logger.log('info', 'sending setup.sh');
|
logger.log('info', 'sending setup.sh');
|
||||||
res.type('application/x-sh');
|
return new Response(this.scripts['setup.sh']
|
||||||
res.send(this.scripts['setup.sh']);
|
.replaceAll('__CLOUDLY_URL__', ctx.url.searchParams.get('cloudlyUrl') || '')
|
||||||
break;
|
.replaceAll('__JUMPCODE__', ctx.url.searchParams.get('jumpcode') || ''), {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-sh',
|
||||||
|
},
|
||||||
|
});
|
||||||
default:
|
default:
|
||||||
res.send('no script found');
|
return new Response('no script found', { status: 404 });
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
constructor(nodeManagerRefArg: CloudlyNodeManager) {
|
constructor(nodeManagerRefArg: CloudlyNodeManager) {
|
||||||
this.nodeManagerRef = nodeManagerRefArg;
|
this.nodeManagerRef = nodeManagerRefArg;
|
||||||
}
|
}
|
||||||
public async getServerUserData(): Promise<string> {
|
public async getServerUserData(clusterArg?: Cluster): Promise<string> {
|
||||||
const sslMode =
|
const sslMode =
|
||||||
await this.nodeManagerRef.cloudlyRef.config.appData.waitForAndGetKey('sslMode');
|
await this.nodeManagerRef.cloudlyRef.config.appData.waitForAndGetKey('sslMode');
|
||||||
let protocol: 'http' | 'https';
|
let protocol: 'http' | 'https';
|
||||||
@@ -80,9 +81,19 @@ bash -c "spark installdaemon"
|
|||||||
const port =
|
const port =
|
||||||
await this.nodeManagerRef.cloudlyRef.config.appData.waitForAndGetKey('publicPort');
|
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
|
const serverUserData = `#cloud-config
|
||||||
runcmd:
|
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);
|
console.log(serverUserData);
|
||||||
return serverUserData;
|
return serverUserData;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export class CloudlyNodeManager {
|
|||||||
public typedRouter = new plugins.typedrequest.TypedRouter();
|
public typedRouter = new plugins.typedrequest.TypedRouter();
|
||||||
public curlfreshInstance = new CurlFresh(this);
|
public curlfreshInstance = new CurlFresh(this);
|
||||||
|
|
||||||
public hetznerAccount: plugins.hetznercloud.HetznerAccount;
|
public hetznerAccount?: plugins.hetznercloud.HetznerAccount;
|
||||||
|
|
||||||
public get db() {
|
public get db() {
|
||||||
return this.cloudlyRef.mongodbConnector.smartdataDb;
|
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'}`);
|
console.log(`Skipping node provisioning for cluster ${cluster.id} - setupMode is ${cluster.data.setupMode || 'manual'}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const hetznerAccount = this.hetznerAccount;
|
||||||
|
if (!hetznerAccount) {
|
||||||
|
throw new Error('Hetzner account is not configured');
|
||||||
|
}
|
||||||
|
|
||||||
// get existing nodes
|
// get existing nodes
|
||||||
const nodes = await this.getNodesByCluster(cluster);
|
const nodes = await this.getNodesByCluster(cluster);
|
||||||
|
|
||||||
// if there is no node, create one
|
// if there is no node, create one
|
||||||
if (nodes.length === 0) {
|
if (nodes.length === 0) {
|
||||||
const hetznerServer = await this.hetznerAccount.createServer({
|
const hetznerServer = await hetznerAccount.createServer({
|
||||||
name: plugins.smartunique.uniSimple('node'),
|
name: plugins.smartunique.uniSimple('node'),
|
||||||
location: 'nbg1',
|
location: 'nbg1',
|
||||||
type: 'cpx41',
|
type: 'cpx41',
|
||||||
@@ -79,7 +83,7 @@ export class CloudlyNodeManager {
|
|||||||
clusterId: cluster.id,
|
clusterId: cluster.id,
|
||||||
priority: '1',
|
priority: '1',
|
||||||
},
|
},
|
||||||
userData: await this.curlfreshInstance.getServerUserData(),
|
userData: await this.curlfreshInstance.getServerUserData(cluster),
|
||||||
});
|
});
|
||||||
|
|
||||||
// First create BareMetal record
|
// First create BareMetal record
|
||||||
@@ -94,12 +98,12 @@ export class CloudlyNodeManager {
|
|||||||
);
|
);
|
||||||
// if there is a node, make sure that it exists
|
// if there is a node, make sure that it exists
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
const hetznerServers = await this.hetznerAccount.getServersByLabel({
|
const hetznerServers = await hetznerAccount.getServersByLabel({
|
||||||
clusterId: cluster.id,
|
clusterId: cluster.id,
|
||||||
});
|
});
|
||||||
if (!hetznerServers || hetznerServers.length === 0) {
|
if (!hetznerServers || hetznerServers.length === 0) {
|
||||||
console.log(`node ${node.id} does not exist in the real world. Creating it now...`);
|
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'),
|
name: plugins.smartunique.uniSimple('node'),
|
||||||
location: 'nbg1',
|
location: 'nbg1',
|
||||||
type: 'cpx41',
|
type: 'cpx41',
|
||||||
@@ -107,6 +111,7 @@ export class CloudlyNodeManager {
|
|||||||
clusterId: cluster.id,
|
clusterId: cluster.id,
|
||||||
priority: '1',
|
priority: '1',
|
||||||
},
|
},
|
||||||
|
userData: await this.curlfreshInstance.getServerUserData(cluster),
|
||||||
});
|
});
|
||||||
|
|
||||||
// First create BareMetal record
|
// First create BareMetal record
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ export class PlatformBinding extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
public static async upsertBinding(
|
public static async upsertBinding(
|
||||||
bindingArg: plugins.servezoneInterfaces.platform.IPlatformBinding,
|
bindingArg: plugins.servezoneInterfaces.platform.IPlatformBinding,
|
||||||
) {
|
) {
|
||||||
const existingBinding =
|
const existingBinding = bindingArg.id
|
||||||
bindingArg.id &&
|
? await this.getInstance({
|
||||||
(await this.getInstance({
|
|
||||||
id: bindingArg.id,
|
id: bindingArg.id,
|
||||||
}));
|
})
|
||||||
|
: undefined;
|
||||||
const binding = existingBinding || new PlatformBinding();
|
const binding = existingBinding || new PlatformBinding();
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
|
|
||||||
|
|||||||
@@ -66,37 +66,32 @@ export class CloudlyRegistryManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async handleHttpRequest(
|
public async handleHttpRequest(
|
||||||
req: plugins.typedserver.Request,
|
ctx: plugins.typedserver.IRequestContext,
|
||||||
res: plugins.typedserver.Response,
|
): Promise<Response> {
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
const requestUrl = new URL((req as any).originalUrl || req.url || '/', 'http://localhost');
|
const requestUrl = ctx.url;
|
||||||
|
|
||||||
if (requestUrl.pathname === '/v2/token') {
|
if (requestUrl.pathname === '/v2/token') {
|
||||||
await this.handleTokenRequest(req, res, requestUrl);
|
return await this.handleTokenRequest(ctx, requestUrl);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.started) {
|
if (!this.started) {
|
||||||
res.status(503);
|
return new Response('registry is not ready', { status: 503 });
|
||||||
res.end('registry is not ready');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawBody = await this.getRawBody(req);
|
const rawBody = Buffer.from(await ctx.request.arrayBuffer());
|
||||||
const response = await this.smartRegistry.handleRequest({
|
const response = await this.smartRegistry.handleRequest({
|
||||||
method: req.method || 'GET',
|
method: ctx.method || 'GET',
|
||||||
path: requestUrl.pathname,
|
path: requestUrl.pathname,
|
||||||
query: Object.fromEntries(requestUrl.searchParams),
|
query: Object.fromEntries(requestUrl.searchParams),
|
||||||
headers: this.headersToRecord(req.headers),
|
headers: this.headersToRecord(ctx.headers),
|
||||||
rawBody: rawBody.length > 0 ? rawBody : undefined,
|
rawBody: rawBody.length > 0 ? rawBody : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.sendRegistryResponse(res, response);
|
return this.createRegistryResponse(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log('error', `registry request failed: ${(error as Error).message}`);
|
logger.log('error', `registry request failed: ${(error as Error).message}`);
|
||||||
res.status(500);
|
return new Response('registry request failed', { status: 500 });
|
||||||
res.end('registry request failed');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,16 +254,17 @@ export class CloudlyRegistryManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async handleTokenRequest(
|
private async handleTokenRequest(
|
||||||
req: plugins.typedserver.Request,
|
ctx: plugins.typedserver.IRequestContext,
|
||||||
res: plugins.typedserver.Response,
|
|
||||||
requestUrl: URL,
|
requestUrl: URL,
|
||||||
) {
|
): Promise<Response> {
|
||||||
const user = await this.authenticateRequest(req);
|
const user = await this.authenticateRequest(ctx);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
res.status(401);
|
return new Response('authentication required', {
|
||||||
res.setHeader('WWW-Authenticate', 'Basic realm="Cloudly Registry"');
|
status: 401,
|
||||||
res.end('authentication required');
|
headers: {
|
||||||
return;
|
'WWW-Authenticate': 'Basic realm="Cloudly Registry"',
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestedScopes = this.getRequestedOciScopes(requestUrl.searchParams);
|
const requestedScopes = this.getRequestedOciScopes(requestUrl.searchParams);
|
||||||
@@ -277,9 +273,7 @@ export class CloudlyRegistryManager {
|
|||||||
return action === 'push' || action === 'delete';
|
return action === 'push' || action === 'delete';
|
||||||
});
|
});
|
||||||
if (requestedWriteAccess && !user.canWrite) {
|
if (requestedWriteAccess && !user.canWrite) {
|
||||||
res.status(403);
|
return new Response('registry write access denied', { status: 403 });
|
||||||
res.end('registry write access denied');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = await this.smartRegistry.getAuthManager().createOciToken(
|
const token = await this.smartRegistry.getAuthManager().createOciToken(
|
||||||
@@ -287,22 +281,26 @@ export class CloudlyRegistryManager {
|
|||||||
requestedScopes,
|
requestedScopes,
|
||||||
3600,
|
3600,
|
||||||
);
|
);
|
||||||
res.status(200);
|
return new Response(
|
||||||
res.setHeader('Content-Type', 'application/json');
|
|
||||||
res.end(
|
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
token,
|
token,
|
||||||
access_token: token,
|
access_token: token,
|
||||||
expires_in: 3600,
|
expires_in: 3600,
|
||||||
issued_at: new Date().toISOString(),
|
issued_at: new Date().toISOString(),
|
||||||
}),
|
}),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async authenticateRequest(
|
private async authenticateRequest(
|
||||||
req: plugins.typedserver.Request,
|
ctx: plugins.typedserver.IRequestContext,
|
||||||
): Promise<TAuthenticatedRegistryUser | null> {
|
): Promise<TAuthenticatedRegistryUser | null> {
|
||||||
const credentials = this.getBasicCredentials(req);
|
const credentials = this.getBasicCredentials(ctx);
|
||||||
if (!credentials) {
|
if (!credentials) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -332,8 +330,8 @@ export class CloudlyRegistryManager {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getBasicCredentials(req: plugins.typedserver.Request) {
|
private getBasicCredentials(ctx: plugins.typedserver.IRequestContext) {
|
||||||
const authHeader = req.headers.authorization;
|
const authHeader = ctx.headers.get('authorization');
|
||||||
if (!authHeader?.startsWith('Basic ')) {
|
if (!authHeader?.startsWith('Basic ')) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -370,48 +368,49 @@ export class CloudlyRegistryManager {
|
|||||||
return `${this.cloudlyRef.config.data.sslMode === 'none' ? 'http' : 'https'}://${this.getRegistryHost()}`;
|
return `${this.cloudlyRef.config.data.sslMode === 'none' ? 'http' : 'https'}://${this.getRegistryHost()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private headersToRecord(headersArg: plugins.typedserver.Request['headers']) {
|
private headersToRecord(headersArg: Headers) {
|
||||||
return Object.fromEntries(
|
const headers: Record<string, string> = {};
|
||||||
Object.entries(headersArg).map(([key, value]) => [
|
headersArg.forEach((value, key) => {
|
||||||
key.toLowerCase(),
|
headers[key.toLowerCase()] = value;
|
||||||
Array.isArray(value) ? value.join(', ') : value || '',
|
});
|
||||||
]),
|
return headers;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getRawBody(req: plugins.typedserver.Request) {
|
private createRegistryResponse(
|
||||||
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,
|
|
||||||
responseArg: plugins.smartregistry.IResponse,
|
responseArg: plugins.smartregistry.IResponse,
|
||||||
) {
|
): Response {
|
||||||
res.status(responseArg.status);
|
const headers = new Headers();
|
||||||
for (const [key, value] of Object.entries(responseArg.headers)) {
|
for (const [key, value] of Object.entries(responseArg.headers)) {
|
||||||
res.setHeader(key, value);
|
headers.set(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!responseArg.body) {
|
if (!responseArg.body) {
|
||||||
res.end();
|
return new Response(null, {
|
||||||
return;
|
status: responseArg.status,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (responseArg.body instanceof ReadableStream) {
|
if (responseArg.body instanceof ReadableStream) {
|
||||||
plugins.stream.Readable.fromWeb(responseArg.body as any).pipe(res);
|
return new Response(responseArg.body, {
|
||||||
return;
|
status: responseArg.status,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Buffer.isBuffer(responseArg.body) || typeof responseArg.body === 'string') {
|
if (Buffer.isBuffer(responseArg.body) || typeof responseArg.body === 'string') {
|
||||||
res.end(responseArg.body);
|
return new Response(responseArg.body as BodyInit, {
|
||||||
return;
|
status: responseArg.status,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
res.setHeader('Content-Type', responseArg.headers['Content-Type'] || 'application/json');
|
if (!headers.has('Content-Type')) {
|
||||||
res.end(JSON.stringify(responseArg.body));
|
headers.set('Content-Type', 'application/json');
|
||||||
|
}
|
||||||
|
return new Response(JSON.stringify(responseArg.body), {
|
||||||
|
status: responseArg.status,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ export class SecretBundle extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
|
|
||||||
// INSTANCE
|
// INSTANCE
|
||||||
@plugins.smartdata.unI()
|
@plugins.smartdata.unI()
|
||||||
public id: string;
|
public id!: string;
|
||||||
|
|
||||||
@plugins.smartdata.svDb()
|
@plugins.smartdata.svDb()
|
||||||
public data: plugins.servezoneInterfaces.data.ISecretBundle['data'];
|
public data!: plugins.servezoneInterfaces.data.ISecretBundle['data'];
|
||||||
|
|
||||||
public async getSecretGroups() {
|
public async getSecretGroups() {
|
||||||
const secretGroups: SecretGroup[] = [];
|
const secretGroups: SecretGroup[] = [];
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ export class SecretGroup extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
* the insatnce id. This should be a random id, except for default
|
* the insatnce id. This should be a random id, except for default
|
||||||
*/
|
*/
|
||||||
@plugins.smartdata.unI()
|
@plugins.smartdata.unI()
|
||||||
id: string;
|
id!: string;
|
||||||
|
|
||||||
@plugins.smartdata.svDb()
|
@plugins.smartdata.svDb()
|
||||||
data: plugins.servezoneInterfaces.data.ISecretGroup['data'];
|
data!: plugins.servezoneInterfaces.data.ISecretGroup['data'];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ export class CloudlySecretManager {
|
|||||||
|
|
||||||
// INSTANCE
|
// INSTANCE
|
||||||
public cloudlyRef: Cloudly;
|
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 serviceQenv = new plugins.qenv.Qenv(paths.packageDir, paths.nogitDir);
|
||||||
public typedrouter: plugins.typedrequest.TypedRouter;
|
public typedrouter!: plugins.typedrequest.TypedRouter;
|
||||||
|
|
||||||
get db() {
|
get db() {
|
||||||
return this.cloudlyRef.mongodbConnector.smartdataDb;
|
return this.cloudlyRef.mongodbConnector.smartdataDb;
|
||||||
@@ -40,7 +40,7 @@ export class CloudlySecretManager {
|
|||||||
new plugins.typedrequest.TypedHandler(
|
new plugins.typedrequest.TypedHandler(
|
||||||
'getSecretBundles',
|
'getSecretBundles',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
await toolsArg.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], dataArg);
|
await toolsArg!.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], dataArg);
|
||||||
dataArg.identity.jwt;
|
dataArg.identity.jwt;
|
||||||
const secretBundles = await SecretBundle.getInstances({});
|
const secretBundles = await SecretBundle.getInstances({});
|
||||||
return {
|
return {
|
||||||
@@ -56,7 +56,7 @@ export class CloudlySecretManager {
|
|||||||
|
|
||||||
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.secretbundle.IReq_GetSecretBundleById>(
|
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.secretbundle.IReq_GetSecretBundleById>(
|
||||||
new plugins.typedrequest.TypedHandler('getSecretBundleById', async (dataArg, toolsArg) => {
|
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({
|
const secretBundle = await SecretBundle.getInstance({
|
||||||
id: dataArg.secretBundleId,
|
id: dataArg.secretBundleId,
|
||||||
});
|
});
|
||||||
@@ -108,7 +108,7 @@ export class CloudlySecretManager {
|
|||||||
new plugins.typedrequest.TypedHandler(
|
new plugins.typedrequest.TypedHandler(
|
||||||
'getSecretGroups',
|
'getSecretGroups',
|
||||||
async (dataArg, toolsArg) => {
|
async (dataArg, toolsArg) => {
|
||||||
await toolsArg.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], dataArg);
|
await toolsArg!.passGuards([this.cloudlyRef.authManager.adminIdentityGuard], dataArg);
|
||||||
dataArg.identity.jwt;
|
dataArg.identity.jwt;
|
||||||
const secretGroups = await SecretGroup.getInstances({});
|
const secretGroups = await SecretGroup.getInstances({});
|
||||||
return {
|
return {
|
||||||
@@ -176,6 +176,9 @@ export class CloudlySecretManager {
|
|||||||
const authorization = await wantedBundle.getAuthorizationFromAuthKey(
|
const authorization = await wantedBundle.getAuthorizationFromAuthKey(
|
||||||
dataArg.secretBundleAuthorization.secretAccessKey,
|
dataArg.secretBundleAuthorization.secretAccessKey,
|
||||||
);
|
);
|
||||||
|
if (!authorization) {
|
||||||
|
throw new plugins.typedrequest.TypedResponseError('secret bundle authorization not found');
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
flatKeyValueObject: await wantedBundle.getKeyValueObjectForEnvironment(
|
flatKeyValueObject: await wantedBundle.getKeyValueObjectForEnvironment(
|
||||||
authorization.environment,
|
authorization.environment,
|
||||||
|
|||||||
@@ -37,10 +37,10 @@ export class Service extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
|
|
||||||
// INSTANCE
|
// INSTANCE
|
||||||
@plugins.smartdata.unI()
|
@plugins.smartdata.unI()
|
||||||
public id: string;
|
public id!: string;
|
||||||
|
|
||||||
@plugins.smartdata.svDb()
|
@plugins.smartdata.svDb()
|
||||||
public data: plugins.servezoneInterfaces.data.IService['data'];
|
public data!: plugins.servezoneInterfaces.data.IService['data'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* a service runs in a specific environment
|
* 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>(
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_CreateService>(
|
||||||
'createService',
|
'createService',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(dataArg, [
|
||||||
|
this.cloudlyRef.authManager.adminIdentityGuard,
|
||||||
|
]);
|
||||||
const service = await Service.createService(dataArg.serviceData);
|
const service = await Service.createService(dataArg.serviceData);
|
||||||
service.data.registryTarget = this.cloudlyRef.registryManager.getServiceRegistryTarget(
|
service.data.registryTarget = this.cloudlyRef.registryManager.getServiceRegistryTarget(
|
||||||
service,
|
service,
|
||||||
@@ -112,6 +115,9 @@ export class ServiceManager {
|
|||||||
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_UpdateService>(
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_UpdateService>(
|
||||||
'updateService',
|
'updateService',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(dataArg, [
|
||||||
|
this.cloudlyRef.authManager.adminIdentityGuard,
|
||||||
|
]);
|
||||||
const service = await Service.getInstance({
|
const service = await Service.getInstance({
|
||||||
id: dataArg.serviceId,
|
id: dataArg.serviceId,
|
||||||
});
|
});
|
||||||
@@ -136,6 +142,9 @@ export class ServiceManager {
|
|||||||
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_DeleteServiceById>(
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_DeleteServiceById>(
|
||||||
'deleteServiceById',
|
'deleteServiceById',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
|
await plugins.smartguard.passGuardsOrReject(dataArg, [
|
||||||
|
this.cloudlyRef.authManager.adminIdentityGuard,
|
||||||
|
]);
|
||||||
const service = await Service.getInstance({
|
const service = await Service.getInstance({
|
||||||
id: dataArg.serviceId,
|
id: dataArg.serviceId,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import * as servezoneInterfaces from '@serve.zone/interfaces';
|
|||||||
export class CloudlySettingsManager {
|
export class CloudlySettingsManager {
|
||||||
public cloudlyRef: Cloudly;
|
public cloudlyRef: Cloudly;
|
||||||
public readyDeferred = plugins.smartpromise.defer();
|
public readyDeferred = plugins.smartpromise.defer();
|
||||||
public settingsStore: plugins.smartdata.EasyStore<servezoneInterfaces.data.ICloudlySettings>;
|
public settingsStore!: plugins.smartdata.EasyStore<servezoneInterfaces.data.ICloudlySettings>;
|
||||||
|
|
||||||
constructor(cloudlyRefArg: Cloudly) {
|
constructor(cloudlyRefArg: Cloudly) {
|
||||||
this.cloudlyRef = cloudlyRefArg;
|
this.cloudlyRef = cloudlyRefArg;
|
||||||
@@ -196,7 +196,7 @@ export class CloudlySettingsManager {
|
|||||||
return { success: false, message: `Unknown provider: ${provider}` };
|
return { success: false, message: `Unknown provider: ${provider}` };
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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'
|
message: 'Settings updated successfully'
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
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`
|
message: `Setting ${requestData.key} cleared successfully`
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: `Failed to clear setting: ${error.message}`
|
message: `Failed to clear setting: ${errorMessage}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,10 +64,10 @@ export class TaskExecution extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
|
|
||||||
// INSTANCE
|
// INSTANCE
|
||||||
@plugins.smartdata.unI()
|
@plugins.smartdata.unI()
|
||||||
public id: string;
|
public id!: string;
|
||||||
|
|
||||||
@plugins.smartdata.svDb()
|
@plugins.smartdata.svDb()
|
||||||
public data: plugins.servezoneInterfaces.data.ITaskExecution['data'];
|
public data!: plugins.servezoneInterfaces.data.ITaskExecution['data'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a log entry to the execution
|
* Add a log entry to the execution
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export class CloudlyTaskManager {
|
|||||||
taskName: string,
|
taskName: string,
|
||||||
triggeredBy: 'schedule' | 'manual' | 'system',
|
triggeredBy: 'schedule' | 'manual' | 'system',
|
||||||
userId?: string
|
userId?: string
|
||||||
): Promise<TaskExecution> {
|
): Promise<TaskExecution | null> {
|
||||||
const task = this.taskRegistry.get(taskName);
|
const task = this.taskRegistry.get(taskName);
|
||||||
const info = this.taskInfo.get(taskName);
|
const info = this.taskInfo.get(taskName);
|
||||||
|
|
||||||
@@ -298,6 +298,9 @@ export class CloudlyTaskManager {
|
|||||||
'manual',
|
'manual',
|
||||||
reqArg.userId
|
reqArg.userId
|
||||||
);
|
);
|
||||||
|
if (!execution) {
|
||||||
|
throw new Error(`Task ${reqArg.taskName} did not start`);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
execution: await execution.createSavableObject(),
|
execution: await execution.createSavableObject(),
|
||||||
@@ -336,6 +339,7 @@ export class CloudlyTaskManager {
|
|||||||
if (deletedCount > 0) {
|
if (deletedCount > 0) {
|
||||||
logger.log('info', `Cleaned up ${deletedCount} old task executions`);
|
logger.log('info', `Cleaned up ${deletedCount} old task executions`);
|
||||||
}
|
}
|
||||||
|
await this.taskBufferManager.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import * as plugins from '../plugins.js';
|
|||||||
import { CloudlyTaskManager } from './classes.taskmanager.js';
|
import { CloudlyTaskManager } from './classes.taskmanager.js';
|
||||||
import { logger } from '../logger.js';
|
import { logger } from '../logger.js';
|
||||||
|
|
||||||
|
const getErrorMessage = (errorArg: unknown) => errorArg instanceof Error ? errorArg.message : String(errorArg);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create and register all predefined tasks
|
* 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');
|
await execution?.addLog(`Cloudflare sync done: ${created} created, ${updated} updated`, 'success');
|
||||||
return { created, updated, totalZones: zones.length };
|
return { created, updated, totalZones: zones.length };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await execution?.addLog(`Cloudflare sync error: ${error.message}`, 'error');
|
await execution?.addLog(`Cloudflare sync error: ${getErrorMessage(error)}`, 'error');
|
||||||
throw 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');
|
await execution?.addLog(`Syncing DNS entry: ${entry.data.name}.${entry.data.zone}`, 'info');
|
||||||
syncedCount++;
|
syncedCount++;
|
||||||
} catch (error) {
|
} 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++;
|
failedCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,7 +135,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
|
|||||||
|
|
||||||
return { synced: syncedCount, failed: failedCount };
|
return { synced: syncedCount, failed: failedCount };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await execution?.addLog(`DNS sync error: ${error.message}`, 'error');
|
await execution?.addLog(`DNS sync error: ${getErrorMessage(error)}`, 'error');
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -194,7 +196,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
|
|||||||
|
|
||||||
return { renewed: renewedCount, upToDate: upToDateCount };
|
return { renewed: renewedCount, upToDate: upToDateCount };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await execution?.addLog(`Certificate renewal error: ${error.message}`, 'error');
|
await execution?.addLog(`Certificate renewal error: ${getErrorMessage(error)}`, 'error');
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -247,7 +249,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
|
|||||||
images: deletedImages,
|
images: deletedImages,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await execution?.addLog(`Cleanup error: ${error.message}`, 'error');
|
await execution?.addLog(`Cleanup error: ${getErrorMessage(error)}`, 'error');
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -279,7 +281,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
|
|||||||
|
|
||||||
let healthyCount = 0;
|
let healthyCount = 0;
|
||||||
let unhealthyCount = 0;
|
let unhealthyCount = 0;
|
||||||
const issues = [];
|
const issues: Array<{ deploymentId: string; serviceId: string; issue: string }> = [];
|
||||||
|
|
||||||
for (const deployment of deployments) {
|
for (const deployment of deployments) {
|
||||||
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) {
|
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 };
|
return { healthy: healthyCount, unhealthy: unhealthyCount, issues };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await execution?.addLog(`Health check error: ${error.message}`, 'error');
|
await execution?.addLog(`Health check error: ${getErrorMessage(error)}`, 'error');
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -345,7 +347,13 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
|
|||||||
// Get all nodes
|
// Get all nodes
|
||||||
const nodes = await taskManager.cloudlyRef.nodeManager.CClusterNode.getInstances({});
|
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(),
|
timestamp: Date.now(),
|
||||||
nodes: [],
|
nodes: [],
|
||||||
totalCpu: 0,
|
totalCpu: 0,
|
||||||
@@ -388,7 +396,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
|
|||||||
|
|
||||||
return report;
|
return report;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await execution?.addLog(`Resource report error: ${error.message}`, 'error');
|
await execution?.addLog(`Resource report error: ${getErrorMessage(error)}`, 'error');
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -428,7 +436,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
|
|||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await execution?.addLog(`Database maintenance error: ${error.message}`, 'error');
|
await execution?.addLog(`Database maintenance error: ${getErrorMessage(error)}`, 'error');
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -454,7 +462,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const vulnerabilities = [];
|
const vulnerabilities: Array<{ type: string; severity: string; image: string; version: string }> = [];
|
||||||
|
|
||||||
// Check for exposed ports
|
// Check for exposed ports
|
||||||
await execution?.addLog('Checking for exposed ports...', 'info');
|
await execution?.addLog('Checking for exposed ports...', 'info');
|
||||||
@@ -497,7 +505,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
|
|||||||
|
|
||||||
return { vulnerabilities };
|
return { vulnerabilities };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await execution?.addLog(`Security scan error: ${error.message}`, 'error');
|
await execution?.addLog(`Security scan error: ${getErrorMessage(error)}`, 'error');
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -549,7 +557,7 @@ export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
|
|||||||
networks: removedNetworks,
|
networks: removedNetworks,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await execution?.addLog(`Docker cleanup error: ${error.message}`, 'error');
|
await execution?.addLog(`Docker cleanup error: ${getErrorMessage(error)}`, 'error');
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+4
-3
@@ -2,8 +2,9 @@
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as crypto from 'node:crypto';
|
import * as crypto from 'node:crypto';
|
||||||
import * as stream from 'stream';
|
import * as stream from 'stream';
|
||||||
|
import * as fsPromises from 'node:fs/promises';
|
||||||
|
|
||||||
export { path, crypto, stream };
|
export { path, crypto, stream, fsPromises };
|
||||||
|
|
||||||
// @apiglobal scope
|
// @apiglobal scope
|
||||||
import * as typedrequest from '@api.global/typedrequest';
|
import * as typedrequest from '@api.global/typedrequest';
|
||||||
@@ -25,7 +26,7 @@ import * as tsclass from '@tsclass/tsclass';
|
|||||||
export { tsclass };
|
export { tsclass };
|
||||||
|
|
||||||
// @push.rocks scope
|
// @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 projectinfo from '@push.rocks/projectinfo';
|
||||||
import * as qenv from '@push.rocks/qenv';
|
import * as qenv from '@push.rocks/qenv';
|
||||||
import * as smartacme from '@push.rocks/smartacme';
|
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';
|
import * as typedserver from '@api.global/typedserver';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
npmextra,
|
smartconfig,
|
||||||
projectinfo,
|
projectinfo,
|
||||||
qenv,
|
qenv,
|
||||||
smartacme,
|
smartacme,
|
||||||
|
|||||||
@@ -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.
|
The package is published from `cloudly/ts_cliclient` via `tspublish.json` under the name `@serve.zone/cli` with the `servezone` binary.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pnpm add -g @serve.zone/cli
|
pnpm add --global @serve.zone/cli
|
||||||
```
|
```
|
||||||
|
|
||||||
For local development inside the Cloudly repository, build the parent package:
|
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
|
## 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
|
```ts
|
||||||
import { CloudlyApiClient } from '@serve.zone/api';
|
import { CloudlyApiClient } from '@serve.zone/api';
|
||||||
|
|||||||
@@ -11,5 +11,6 @@
|
|||||||
"registry.npmjs.org:public",
|
"registry.npmjs.org:public",
|
||||||
"verdaccio.lossless.digital:public"
|
"verdaccio.lossless.digital:public"
|
||||||
],
|
],
|
||||||
"bin": ["servezone"]
|
"bin": ["servezone"],
|
||||||
|
"order": 10
|
||||||
}
|
}
|
||||||
+121
-117
@@ -3,7 +3,7 @@ import * as domtools from '@design.estate/dees-domtools';
|
|||||||
|
|
||||||
const appstate = new plugins.deesDomtools.plugins.smartstate.Smartstate();
|
const appstate = new plugins.deesDomtools.plugins.smartstate.Smartstate();
|
||||||
export interface ILoginState {
|
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>(
|
export const loginStatePart: plugins.smartstate.StatePart<unknown, ILoginState> = await appstate.getStatePart<ILoginState>(
|
||||||
'login',
|
'login',
|
||||||
@@ -13,8 +13,8 @@ export const loginStatePart: plugins.smartstate.StatePart<unknown, ILoginState>
|
|||||||
|
|
||||||
export const loginAction = loginStatePart.createAction<{ username: string; password: string }>(
|
export const loginAction = loginStatePart.createAction<{ username: string; password: string }>(
|
||||||
async (statePartArg, payloadArg) => {
|
async (statePartArg, payloadArg) => {
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState() || { identity: null };
|
||||||
let identity: plugins.interfaces.data.IIdentity = null;
|
let identity: plugins.interfaces.data.IIdentity | null = null;
|
||||||
try {
|
try {
|
||||||
identity = await apiClient.loginWithUsernameAndPassword(payloadArg.username, payloadArg.password);
|
identity = await apiClient.loginWithUsernameAndPassword(payloadArg.username, payloadArg.password);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -31,7 +31,7 @@ export const loginAction = loginStatePart.createAction<{ username: string; passw
|
|||||||
if (!apiClient['typedsocketClient']) {
|
if (!apiClient['typedsocketClient']) {
|
||||||
await apiClient.start();
|
await apiClient.start();
|
||||||
}
|
}
|
||||||
try { apiClient.typedsocketClient.addTag('identity', apiClient.identity); } catch {}
|
try { await apiClient.typedsocketClient.setTag('identity', apiClient.identity); } catch {}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
return newState;
|
return newState;
|
||||||
@@ -39,7 +39,7 @@ export const loginAction = loginStatePart.createAction<{ username: string; passw
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const logoutAction = loginStatePart.createAction(async (statePartArg) => {
|
export const logoutAction = loginStatePart.createAction(async (statePartArg) => {
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState() || { identity: null };
|
||||||
return {
|
return {
|
||||||
...currentState,
|
...currentState,
|
||||||
identity: null,
|
identity: null,
|
||||||
@@ -88,17 +88,21 @@ export const dataState = await appstate.getStatePart<IDataState>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Shared API client instance (used by UI actions)
|
// 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({
|
export const apiClient = new plugins.servezoneApi.CloudlyApiClient({
|
||||||
registerAs: 'api',
|
registerAs: 'api',
|
||||||
cloudlyUrl: (typeof window !== 'undefined' && window.location?.origin) ? window.location.origin : undefined,
|
cloudlyUrl: (typeof window !== 'undefined' && window.location?.origin) ? window.location.origin : undefined,
|
||||||
});
|
}) as TCloudlyApiClientWithNullableIdentity;
|
||||||
|
|
||||||
// Getting data
|
// Getting data
|
||||||
export const getAllDataAction = dataState.createAction(async (statePartArg) => {
|
export const getAllDataAction = dataState.createAction(async (statePartArg) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState() || {};
|
||||||
// SecretsGroups
|
// SecretsGroups
|
||||||
try {
|
try {
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
const secretGroups = await apiClient.secretgroup.getSecretGroups();
|
const secretGroups = await apiClient.secretgroup.getSecretGroups();
|
||||||
currentState = {
|
currentState = {
|
||||||
...currentState,
|
...currentState,
|
||||||
@@ -114,7 +118,7 @@ export const getAllDataAction = dataState.createAction(async (statePartArg) => {
|
|||||||
|
|
||||||
// SecretBundles
|
// SecretBundles
|
||||||
try {
|
try {
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
const responseSecretBundles = await apiClient.secretbundle.getSecretBundles();
|
const responseSecretBundles = await apiClient.secretbundle.getSecretBundles();
|
||||||
currentState = {
|
currentState = {
|
||||||
...currentState,
|
...currentState,
|
||||||
@@ -130,7 +134,7 @@ export const getAllDataAction = dataState.createAction(async (statePartArg) => {
|
|||||||
|
|
||||||
// images
|
// images
|
||||||
try {
|
try {
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
const images = await apiClient.image.getImages();
|
const images = await apiClient.image.getImages();
|
||||||
currentState = {
|
currentState = {
|
||||||
...currentState,
|
...currentState,
|
||||||
@@ -146,7 +150,7 @@ export const getAllDataAction = dataState.createAction(async (statePartArg) => {
|
|||||||
|
|
||||||
// Clusters
|
// Clusters
|
||||||
try {
|
try {
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
const clusters = await apiClient.cluster.getClusters();
|
const clusters = await apiClient.cluster.getClusters();
|
||||||
currentState = {
|
currentState = {
|
||||||
...currentState,
|
...currentState,
|
||||||
@@ -162,7 +166,7 @@ export const getAllDataAction = dataState.createAction(async (statePartArg) => {
|
|||||||
|
|
||||||
// External Registries via shared API client
|
// External Registries via shared API client
|
||||||
try {
|
try {
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
const registries = await apiClient.externalRegistry.getRegistries();
|
const registries = await apiClient.externalRegistry.getRegistries();
|
||||||
currentState = {
|
currentState = {
|
||||||
...currentState,
|
...currentState,
|
||||||
@@ -178,7 +182,7 @@ export const getAllDataAction = dataState.createAction(async (statePartArg) => {
|
|||||||
|
|
||||||
// Services
|
// Services
|
||||||
try {
|
try {
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
const services = await apiClient.services.getServices();
|
const services = await apiClient.services.getServices();
|
||||||
currentState = {
|
currentState = {
|
||||||
...currentState,
|
...currentState,
|
||||||
@@ -194,7 +198,7 @@ export const getAllDataAction = dataState.createAction(async (statePartArg) => {
|
|||||||
|
|
||||||
// Deployments
|
// Deployments
|
||||||
try {
|
try {
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
const responseDeployments = await apiClient.deployments.getDeployments();
|
const responseDeployments = await apiClient.deployments.getDeployments();
|
||||||
currentState = {
|
currentState = {
|
||||||
...currentState,
|
...currentState,
|
||||||
@@ -210,7 +214,7 @@ export const getAllDataAction = dataState.createAction(async (statePartArg) => {
|
|||||||
|
|
||||||
// Domains via API client
|
// Domains via API client
|
||||||
try {
|
try {
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
const responseDomains = await apiClient.domains.getDomains();
|
const responseDomains = await apiClient.domains.getDomains();
|
||||||
currentState = {
|
currentState = {
|
||||||
...currentState,
|
...currentState,
|
||||||
@@ -226,7 +230,7 @@ export const getAllDataAction = dataState.createAction(async (statePartArg) => {
|
|||||||
|
|
||||||
// DNS Entries via API client
|
// DNS Entries via API client
|
||||||
try {
|
try {
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
const responseDnsEntries = await apiClient.dns.getDnsEntries();
|
const responseDnsEntries = await apiClient.dns.getDnsEntries();
|
||||||
currentState = {
|
currentState = {
|
||||||
...currentState,
|
...currentState,
|
||||||
@@ -245,58 +249,57 @@ export const getAllDataAction = dataState.createAction(async (statePartArg) => {
|
|||||||
|
|
||||||
// Service Actions
|
// Service Actions
|
||||||
export const createServiceAction = dataState.createAction(
|
export const createServiceAction = dataState.createAction(
|
||||||
async (statePartArg, payloadArg: { serviceData: plugins.interfaces.data.IService['data'] }) => {
|
async (statePartArg, payloadArg: { serviceData: plugins.interfaces.data.IService['data'] }, context) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState() || {};
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
await apiClient.services.createService(payloadArg.serviceData);
|
await apiClient.services.createService(payloadArg.serviceData);
|
||||||
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
currentState = await context.dispatch(getAllDataAction, null);
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const updateServiceAction = dataState.createAction(
|
export const updateServiceAction = dataState.createAction(
|
||||||
async (statePartArg, payloadArg: { serviceId: string; serviceData: plugins.interfaces.data.IService['data'] }) => {
|
async (statePartArg, payloadArg: { serviceId: string; serviceData: plugins.interfaces.data.IService['data'] }, context) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState() || {};
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
await apiClient.services.updateService(payloadArg.serviceId, payloadArg.serviceData);
|
await apiClient.services.updateService(payloadArg.serviceId, payloadArg.serviceData);
|
||||||
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
currentState = await context.dispatch(getAllDataAction, null);
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const deleteServiceAction = dataState.createAction(
|
export const deleteServiceAction = dataState.createAction(
|
||||||
async (statePartArg, payloadArg: { serviceId: string }) => {
|
async (statePartArg, payloadArg: { serviceId: string }, context) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState() || {};
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
await apiClient.services.deleteService(payloadArg.serviceId);
|
await apiClient.services.deleteService(payloadArg.serviceId);
|
||||||
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
currentState = await context.dispatch(getAllDataAction, null);
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// SecretGroup Actions
|
// SecretGroup Actions
|
||||||
export const createSecretGroupAction = dataState.createAction(
|
export const createSecretGroupAction = dataState.createAction(
|
||||||
async (statePartArg, payloadArg: plugins.interfaces.data.ISecretGroup) => {
|
async (statePartArg, payloadArg: { data: plugins.interfaces.data.ISecretGroup['data'] }, context) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState() || {};
|
||||||
try {
|
try {
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
await apiClient.secretgroup.createSecretGroup(payloadArg.data);
|
await apiClient.secretgroup.createSecretGroup(payloadArg.data);
|
||||||
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
currentState = await context.dispatch(getAllDataAction, null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to create secret group:', err);
|
console.error('Failed to create secret group:', err);
|
||||||
}
|
}
|
||||||
return currentState;
|
return currentState;
|
||||||
return currentState;
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const deleteSecretGroupAction = dataState.createAction(
|
export const deleteSecretGroupAction = dataState.createAction(
|
||||||
async (statePartArg, payloadArg: { secretGroupId: string }) => {
|
async (statePartArg, payloadArg: { secretGroupId: string }, context) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState() || {};
|
||||||
try {
|
try {
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
await apiClient.secretgroup.deleteSecretGroupById(payloadArg.secretGroupId);
|
await apiClient.secretgroup.deleteSecretGroupById(payloadArg.secretGroupId);
|
||||||
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
currentState = await context.dispatch(getAllDataAction, null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to delete secret group:', err);
|
console.error('Failed to delete secret group:', err);
|
||||||
}
|
}
|
||||||
@@ -306,12 +309,12 @@ export const deleteSecretGroupAction = dataState.createAction(
|
|||||||
|
|
||||||
// SecretBundle Actions
|
// SecretBundle Actions
|
||||||
export const deleteSecretBundleAction = dataState.createAction(
|
export const deleteSecretBundleAction = dataState.createAction(
|
||||||
async (statePartArg, payloadArg: { configBundleId: string }) => {
|
async (statePartArg, payloadArg: { configBundleId: string }, context) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState() || {};
|
||||||
try {
|
try {
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
await apiClient.secretbundle.deleteSecretBundleById(payloadArg.configBundleId);
|
await apiClient.secretbundle.deleteSecretBundleById(payloadArg.configBundleId);
|
||||||
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
currentState = await context.dispatch(getAllDataAction, null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to delete secret bundle:', err);
|
console.error('Failed to delete secret bundle:', err);
|
||||||
}
|
}
|
||||||
@@ -321,146 +324,146 @@ export const deleteSecretBundleAction = dataState.createAction(
|
|||||||
|
|
||||||
// image actions
|
// image actions
|
||||||
export const createImageAction = dataState.createAction(
|
export const createImageAction = dataState.createAction(
|
||||||
async (statePartArg, payloadArg: { imageName: string, description: string }) => {
|
async (statePartArg, payloadArg: { imageName: string, description: string }, context) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState() || {};
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
await apiClient.image.createImage({ name: payloadArg.imageName, description: payloadArg.description });
|
await apiClient.image.createImage({ name: payloadArg.imageName, description: payloadArg.description });
|
||||||
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
currentState = await context.dispatch(getAllDataAction, null);
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const deleteImageAction = dataState.createAction(
|
export const deleteImageAction = dataState.createAction(
|
||||||
async (statePartArg, payloadArg: { imageId: string }) => {
|
async (statePartArg, payloadArg: { imageId: string }, context) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState() || {};
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
await apiClient.image.deleteImage(payloadArg.imageId);
|
await apiClient.image.deleteImage(payloadArg.imageId);
|
||||||
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
currentState = await context.dispatch(getAllDataAction, null);
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Deployment Actions
|
// Deployment Actions
|
||||||
export const createDeploymentAction = dataState.createAction(
|
export const createDeploymentAction = dataState.createAction(
|
||||||
async (statePartArg, payloadArg: { deploymentData: Partial<plugins.interfaces.data.IDeployment> }) => {
|
async (statePartArg, payloadArg: { deploymentData: Partial<plugins.interfaces.data.IDeployment> }, context) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState() || {};
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
await apiClient.deployments.createDeployment(payloadArg.deploymentData);
|
await apiClient.deployments.createDeployment(payloadArg.deploymentData);
|
||||||
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
currentState = await context.dispatch(getAllDataAction, null);
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const updateDeploymentAction = dataState.createAction(
|
export const updateDeploymentAction = dataState.createAction(
|
||||||
async (statePartArg, payloadArg: { deploymentId: string; deploymentData: Partial<plugins.interfaces.data.IDeployment> }) => {
|
async (statePartArg, payloadArg: { deploymentId: string; deploymentData: Partial<plugins.interfaces.data.IDeployment> }, context) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState() || {};
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
await apiClient.deployments.updateDeployment(payloadArg.deploymentId, payloadArg.deploymentData);
|
await apiClient.deployments.updateDeployment(payloadArg.deploymentId, payloadArg.deploymentData);
|
||||||
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
currentState = await context.dispatch(getAllDataAction, null);
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const deleteDeploymentAction = dataState.createAction(
|
export const deleteDeploymentAction = dataState.createAction(
|
||||||
async (statePartArg, payloadArg: { deploymentId: string }) => {
|
async (statePartArg, payloadArg: { deploymentId: string }, context) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState() || {};
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
await apiClient.deployments.deleteDeployment(payloadArg.deploymentId);
|
await apiClient.deployments.deleteDeployment(payloadArg.deploymentId);
|
||||||
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
currentState = await context.dispatch(getAllDataAction, null);
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// DNS Actions
|
// DNS Actions
|
||||||
export const createDnsEntryAction = dataState.createAction(
|
export const createDnsEntryAction = dataState.createAction(
|
||||||
async (statePartArg, payloadArg: { dnsEntryData: plugins.interfaces.data.IDnsEntry['data'] }) => {
|
async (statePartArg, payloadArg: { dnsEntryData: plugins.interfaces.data.IDnsEntry['data'] }, context) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState() || {};
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
await apiClient.dns.createDnsEntry(payloadArg.dnsEntryData);
|
await apiClient.dns.createDnsEntry(payloadArg.dnsEntryData);
|
||||||
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
currentState = await context.dispatch(getAllDataAction, null);
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const updateDnsEntryAction = dataState.createAction(
|
export const updateDnsEntryAction = dataState.createAction(
|
||||||
async (statePartArg, payloadArg: { dnsEntryId: string; dnsEntryData: plugins.interfaces.data.IDnsEntry['data'] }) => {
|
async (statePartArg, payloadArg: { dnsEntryId: string; dnsEntryData: plugins.interfaces.data.IDnsEntry['data'] }, context) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState() || {};
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
await apiClient.dns.updateDnsEntry(payloadArg.dnsEntryId, payloadArg.dnsEntryData);
|
await apiClient.dns.updateDnsEntry(payloadArg.dnsEntryId, payloadArg.dnsEntryData);
|
||||||
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
currentState = await context.dispatch(getAllDataAction, null);
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const deleteDnsEntryAction = dataState.createAction(
|
export const deleteDnsEntryAction = dataState.createAction(
|
||||||
async (statePartArg, payloadArg: { dnsEntryId: string }) => {
|
async (statePartArg, payloadArg: { dnsEntryId: string }, context) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState() || {};
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
await apiClient.dns.deleteDnsEntry(payloadArg.dnsEntryId);
|
await apiClient.dns.deleteDnsEntry(payloadArg.dnsEntryId);
|
||||||
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
currentState = await context.dispatch(getAllDataAction, null);
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Domain Actions
|
// Domain Actions
|
||||||
export const createDomainAction = dataState.createAction(
|
export const createDomainAction = dataState.createAction(
|
||||||
async (statePartArg, payloadArg: { domainData: plugins.interfaces.data.IDomain['data'] }) => {
|
async (statePartArg, payloadArg: { domainData: plugins.interfaces.data.IDomain['data'] }, context) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState() || {};
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
await apiClient.domains.createDomain(payloadArg.domainData);
|
await apiClient.domains.createDomain(payloadArg.domainData);
|
||||||
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
currentState = await context.dispatch(getAllDataAction, null);
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const updateDomainAction = dataState.createAction(
|
export const updateDomainAction = dataState.createAction(
|
||||||
async (statePartArg, payloadArg: { domainId: string; domainData: plugins.interfaces.data.IDomain['data'] }) => {
|
async (statePartArg, payloadArg: { domainId: string; domainData: plugins.interfaces.data.IDomain['data'] }, context) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState() || {};
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
await apiClient.domains.updateDomain(payloadArg.domainId, payloadArg.domainData);
|
await apiClient.domains.updateDomain(payloadArg.domainId, payloadArg.domainData);
|
||||||
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
currentState = await context.dispatch(getAllDataAction, null);
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const deleteDomainAction = dataState.createAction(
|
export const deleteDomainAction = dataState.createAction(
|
||||||
async (statePartArg, payloadArg: { domainId: string }) => {
|
async (statePartArg, payloadArg: { domainId: string }, context) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState() || {};
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
await apiClient.domains.deleteDomain(payloadArg.domainId);
|
await apiClient.domains.deleteDomain(payloadArg.domainId);
|
||||||
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
currentState = await context.dispatch(getAllDataAction, null);
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const verifyDomainAction = dataState.createAction(
|
export const verifyDomainAction = dataState.createAction(
|
||||||
async (statePartArg, payloadArg: { domainId: string; verificationMethod?: 'dns' | 'http' | 'email' | 'manual' }) => {
|
async (statePartArg, payloadArg: { domainId: string; verificationMethod?: 'dns' | 'http' | 'email' | 'manual' }, context) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState() || {};
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
await apiClient.domains.verifyDomain(payloadArg.domainId, payloadArg.verificationMethod);
|
await apiClient.domains.verifyDomain(payloadArg.domainId, payloadArg.verificationMethod);
|
||||||
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
currentState = await context.dispatch(getAllDataAction, null);
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// External Registry Actions
|
// External Registry Actions
|
||||||
export const createExternalRegistryAction = dataState.createAction(
|
export const createExternalRegistryAction = dataState.createAction(
|
||||||
async (statePartArg, payloadArg: { registryData: plugins.interfaces.data.IExternalRegistry['data'] }) => {
|
async (statePartArg, payloadArg: { registryData: plugins.interfaces.data.IExternalRegistry['data'] }, context) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState() || {};
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
await apiClient.externalRegistry.createRegistry(payloadArg.registryData);
|
await apiClient.externalRegistry.createRegistry(payloadArg.registryData);
|
||||||
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
currentState = await context.dispatch(getAllDataAction, null);
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const updateExternalRegistryAction = dataState.createAction(
|
export const updateExternalRegistryAction = dataState.createAction(
|
||||||
async (statePartArg, payloadArg: { registryId: string; registryData: plugins.interfaces.data.IExternalRegistry['data'] }) => {
|
async (statePartArg, payloadArg: { registryId: string; registryData: plugins.interfaces.data.IExternalRegistry['data'] }, context) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState() || {};
|
||||||
try {
|
try {
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
await apiClient.externalRegistry.updateRegistry(payloadArg.registryId, payloadArg.registryData);
|
await apiClient.externalRegistry.updateRegistry(payloadArg.registryId, payloadArg.registryData);
|
||||||
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
currentState = await context.dispatch(getAllDataAction, null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to update external registry:', err);
|
console.error('Failed to update external registry:', err);
|
||||||
}
|
}
|
||||||
@@ -469,12 +472,12 @@ export const updateExternalRegistryAction = dataState.createAction(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const deleteExternalRegistryAction = dataState.createAction(
|
export const deleteExternalRegistryAction = dataState.createAction(
|
||||||
async (statePartArg, payloadArg: { registryId: string }) => {
|
async (statePartArg, payloadArg: { registryId: string }, context) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState() || {};
|
||||||
try {
|
try {
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
await apiClient.externalRegistry.deleteRegistry(payloadArg.registryId);
|
await apiClient.externalRegistry.deleteRegistry(payloadArg.registryId);
|
||||||
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
currentState = await context.dispatch(getAllDataAction, null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to delete external registry:', err);
|
console.error('Failed to delete external registry:', err);
|
||||||
}
|
}
|
||||||
@@ -484,9 +487,9 @@ export const deleteExternalRegistryAction = dataState.createAction(
|
|||||||
|
|
||||||
export const verifyExternalRegistryAction = dataState.createAction(
|
export const verifyExternalRegistryAction = dataState.createAction(
|
||||||
async (statePartArg, payloadArg: { registryId: string }) => {
|
async (statePartArg, payloadArg: { registryId: string }) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState() || {};
|
||||||
try {
|
try {
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
const result = await apiClient.externalRegistry.verifyRegistry(payloadArg.registryId);
|
const result = await apiClient.externalRegistry.verifyRegistry(payloadArg.registryId);
|
||||||
if (result.success && result.registry) {
|
if (result.success && result.registry) {
|
||||||
const regs = (currentState.externalRegistries || []).slice();
|
const regs = (currentState.externalRegistries || []).slice();
|
||||||
@@ -514,8 +517,8 @@ export const verifyExternalRegistryAction = dataState.createAction(
|
|||||||
export const taskActions = {
|
export const taskActions = {
|
||||||
getTasks: dataState.createAction(
|
getTasks: dataState.createAction(
|
||||||
async (statePartArg, payloadArg: {}) => {
|
async (statePartArg, payloadArg: {}) => {
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState() || {};
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
const response = await apiClient.tasks.getTasks();
|
const response = await apiClient.tasks.getTasks();
|
||||||
return {
|
return {
|
||||||
...currentState,
|
...currentState,
|
||||||
@@ -526,8 +529,8 @@ export const taskActions = {
|
|||||||
|
|
||||||
getTaskExecutions: dataState.createAction(
|
getTaskExecutions: dataState.createAction(
|
||||||
async (statePartArg, payloadArg: { filter?: any }) => {
|
async (statePartArg, payloadArg: { filter?: any }) => {
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState() || {};
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
const response = await apiClient.tasks.getTaskExecutions(payloadArg.filter);
|
const response = await apiClient.tasks.getTaskExecutions(payloadArg.filter);
|
||||||
return {
|
return {
|
||||||
...currentState,
|
...currentState,
|
||||||
@@ -538,8 +541,8 @@ export const taskActions = {
|
|||||||
|
|
||||||
getTaskExecutionById: dataState.createAction(
|
getTaskExecutionById: dataState.createAction(
|
||||||
async (statePartArg, payloadArg: { executionId: string }) => {
|
async (statePartArg, payloadArg: { executionId: string }) => {
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState() || {};
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
await apiClient.tasks.getTaskExecutionById(payloadArg.executionId);
|
await apiClient.tasks.getTaskExecutionById(payloadArg.executionId);
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
@@ -547,8 +550,8 @@ export const taskActions = {
|
|||||||
|
|
||||||
triggerTask: dataState.createAction(
|
triggerTask: dataState.createAction(
|
||||||
async (statePartArg, payloadArg: { taskName: string; userId?: string }) => {
|
async (statePartArg, payloadArg: { taskName: string; userId?: string }) => {
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState() || {};
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
await apiClient.tasks.triggerTask(payloadArg.taskName, payloadArg.userId);
|
await apiClient.tasks.triggerTask(payloadArg.taskName, payloadArg.userId);
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
@@ -556,8 +559,8 @@ export const taskActions = {
|
|||||||
|
|
||||||
cancelTask: dataState.createAction(
|
cancelTask: dataState.createAction(
|
||||||
async (statePartArg, payloadArg: { executionId: string }) => {
|
async (statePartArg, payloadArg: { executionId: string }) => {
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState() || {};
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
await apiClient.tasks.cancelTask(payloadArg.executionId);
|
await apiClient.tasks.cancelTask(payloadArg.executionId);
|
||||||
return currentState;
|
return currentState;
|
||||||
}
|
}
|
||||||
@@ -571,11 +574,12 @@ export const addClusterAction = dataState.createAction(
|
|||||||
payloadArg: {
|
payloadArg: {
|
||||||
clusterName: string;
|
clusterName: string;
|
||||||
setupMode?: 'manual' | 'hetzner' | 'aws' | 'digitalocean';
|
setupMode?: 'manual' | 'hetzner' | 'aws' | 'digitalocean';
|
||||||
}
|
},
|
||||||
|
context
|
||||||
) => {
|
) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState() || {};
|
||||||
apiClient.identity = loginStatePart.getState().identity;
|
apiClient.identity = loginStatePart.getState()?.identity ?? null;
|
||||||
await apiClient.cluster.createClusterAdvanced(payloadArg.clusterName, payloadArg.setupMode);
|
await apiClient.cluster.createClusterAdvanced(payloadArg.clusterName, payloadArg.setupMode);
|
||||||
return await dataState.dispatchAction(getAllDataAction, null);
|
return await context.dispatch(getAllDataAction, null);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ declare global {
|
|||||||
|
|
||||||
@customElement('cloudly-dashboard')
|
@customElement('cloudly-dashboard')
|
||||||
export class CloudlyDashboard extends DeesElement {
|
export class CloudlyDashboard extends DeesElement {
|
||||||
@state() private identity: plugins.interfaces.data.IIdentity;
|
@state() private accessor identity: plugins.interfaces.data.IIdentity | null = null;
|
||||||
@state() private data: appstate.IDataState = {
|
@state() private accessor data: appstate.IDataState = {
|
||||||
secretGroups: [],
|
secretGroups: [],
|
||||||
secretBundles: [],
|
secretBundles: [],
|
||||||
clusters: [],
|
clusters: [],
|
||||||
@@ -188,10 +188,11 @@ export class CloudlyDashboard extends DeesElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
public async firstUpdated() {
|
public async firstUpdated() {
|
||||||
const simpleLogin = this.shadowRoot.querySelector('dees-simple-login');
|
const simpleLogin = this.shadowRoot!.querySelector('dees-simple-login') as any;
|
||||||
simpleLogin.addEventListener('login', (e: CustomEvent) => {
|
simpleLogin.addEventListener('login', (eventArg: Event) => {
|
||||||
console.log(e.detail);
|
const loginEvent = eventArg as CustomEvent;
|
||||||
this.login(e.detail.data.username, e.detail.data.password);
|
console.log(loginEvent.detail);
|
||||||
|
this.login(loginEvent.detail.data.username, loginEvent.detail.data.password);
|
||||||
});
|
});
|
||||||
this.addEventListener('contextmenu', (eventArg) => {
|
this.addEventListener('contextmenu', (eventArg) => {
|
||||||
plugins.deesCatalog.DeesContextmenu.openContextMenuWithOptions(eventArg, [
|
plugins.deesCatalog.DeesContextmenu.openContextMenuWithOptions(eventArg, [
|
||||||
@@ -205,8 +206,8 @@ export class CloudlyDashboard extends DeesElement {
|
|||||||
menuOptions: [
|
menuOptions: [
|
||||||
{
|
{
|
||||||
name: 'close',
|
name: 'close',
|
||||||
iconName: null,
|
iconName: undefined,
|
||||||
action: async (modalArg) => {
|
action: async (modalArg: any) => {
|
||||||
await modalArg.destroy();
|
await modalArg.destroy();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -221,14 +222,14 @@ export class CloudlyDashboard extends DeesElement {
|
|||||||
const domtools = await this.domtoolsPromise;
|
const domtools = await this.domtoolsPromise;
|
||||||
const loginState = appstate.loginStatePart.getState();
|
const loginState = appstate.loginStatePart.getState();
|
||||||
console.log(loginState);
|
console.log(loginState);
|
||||||
if (loginState.identity) {
|
if (loginState?.identity) {
|
||||||
this.identity = loginState.identity;
|
this.identity = loginState.identity;
|
||||||
try {
|
try {
|
||||||
appstate.apiClient.identity = loginState.identity;
|
appstate.apiClient.identity = loginState.identity;
|
||||||
if (!appstate.apiClient['typedsocketClient']) {
|
if (!appstate.apiClient['typedsocketClient']) {
|
||||||
await appstate.apiClient.start();
|
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); }
|
} catch (e) { console.warn('Failed to initialize API client WS', e); }
|
||||||
await simpleLogin.switchToSlottedContent();
|
await simpleLogin.switchToSlottedContent();
|
||||||
await appstate.dataState.dispatchAction(appstate.getAllDataAction, null);
|
await appstate.dataState.dispatchAction(appstate.getAllDataAction, null);
|
||||||
@@ -238,14 +239,14 @@ export class CloudlyDashboard extends DeesElement {
|
|||||||
private async login(username: string, password: string) {
|
private async login(username: string, password: string) {
|
||||||
const domtools = await this.domtoolsPromise;
|
const domtools = await this.domtoolsPromise;
|
||||||
console.log(`attempting to login...`);
|
console.log(`attempting to login...`);
|
||||||
const simpleLogin = this.shadowRoot.querySelector('dees-simple-login');
|
const simpleLogin = this.shadowRoot!.querySelector('dees-simple-login') as any;
|
||||||
const form = simpleLogin.shadowRoot.querySelector('dees-form');
|
const form = simpleLogin.shadowRoot.querySelector('dees-form') as any;
|
||||||
form.setStatus('pending', 'Logging in...');
|
form.setStatus('pending', 'Logging in...');
|
||||||
const state = await appstate.loginStatePart.dispatchAction(appstate.loginAction, {
|
const state = await appstate.loginStatePart.dispatchAction(appstate.loginAction, {
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
if (state.identity) {
|
if (state?.identity) {
|
||||||
console.log('got jwt');
|
console.log('got jwt');
|
||||||
this.identity = state.identity;
|
this.identity = state.identity;
|
||||||
form.setStatus('success', 'Logged in!');
|
form.setStatus('success', 'Logged in!');
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import * as appstate from '../../../appstate.js';
|
|||||||
@customElement('cloudly-view-backups')
|
@customElement('cloudly-view-backups')
|
||||||
export class CloudlyViewBackups extends DeesElement {
|
export class CloudlyViewBackups extends DeesElement {
|
||||||
@state()
|
@state()
|
||||||
private data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
|
private accessor data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ const sourcePresetArchitectures: Record<TBaseOsImageSourcePreset, string> = {
|
|||||||
|
|
||||||
@customElement('cloudly-view-baseos')
|
@customElement('cloudly-view-baseos')
|
||||||
export class CloudlyViewBaseOs extends DeesElement {
|
export class CloudlyViewBaseOs extends DeesElement {
|
||||||
@state() private builds: TBaseOsImageBuild[] = [];
|
@state() private accessor builds: TBaseOsImageBuild[] = [];
|
||||||
@state() private isLoading = false;
|
@state() private accessor isLoading = false;
|
||||||
|
|
||||||
private refreshTimer?: number;
|
private refreshTimer?: number;
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import * as appstate from '../../../appstate.js';
|
|||||||
@customElement('cloudly-view-clusters')
|
@customElement('cloudly-view-clusters')
|
||||||
export class CloudlyViewClusters extends DeesElement {
|
export class CloudlyViewClusters extends DeesElement {
|
||||||
@state()
|
@state()
|
||||||
private data: appstate.IDataState = {} as any;
|
private accessor data: appstate.IDataState = {} as any;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import * as appstate from '../../../appstate.js';
|
|||||||
@customElement('cloudly-view-dbs')
|
@customElement('cloudly-view-dbs')
|
||||||
export class CloudlyViewDbs extends DeesElement {
|
export class CloudlyViewDbs extends DeesElement {
|
||||||
@state()
|
@state()
|
||||||
private data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
|
private accessor data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import * as appstate from '../../../appstate.js';
|
|||||||
@customElement('cloudly-view-deployments')
|
@customElement('cloudly-view-deployments')
|
||||||
export class CloudlyViewDeployments extends DeesElement {
|
export class CloudlyViewDeployments extends DeesElement {
|
||||||
@state()
|
@state()
|
||||||
private data: appstate.IDataState = {} as any;
|
private accessor data: appstate.IDataState = {} as any;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -219,4 +219,3 @@ declare global {
|
|||||||
'cloudly-view-deployments': CloudlyViewDeployments;
|
'cloudly-view-deployments': CloudlyViewDeployments;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import * as appstate from '../../../appstate.js';
|
|||||||
@customElement('cloudly-view-dns')
|
@customElement('cloudly-view-dns')
|
||||||
export class CloudlyViewDns extends DeesElement {
|
export class CloudlyViewDns extends DeesElement {
|
||||||
@state()
|
@state()
|
||||||
private data: appstate.IDataState = { secretGroups: [], secretBundles: [], dnsEntries: [], domains: [] } as any;
|
private accessor data: appstate.IDataState = { secretGroups: [], secretBundles: [], dnsEntries: [], domains: [] } as any;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import * as appstate from '../../../appstate.js';
|
|||||||
@customElement('cloudly-view-domains')
|
@customElement('cloudly-view-domains')
|
||||||
export class CloudlyViewDomains extends DeesElement {
|
export class CloudlyViewDomains extends DeesElement {
|
||||||
@state()
|
@state()
|
||||||
private data: appstate.IDataState = { secretGroups: [], secretBundles: [], domains: [], dnsEntries: [] } as any;
|
private accessor data: appstate.IDataState = { secretGroups: [], secretBundles: [], domains: [], dnsEntries: [] } as any;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import * as appstate from '../../../appstate.js';
|
|||||||
@customElement('cloudly-view-externalregistries')
|
@customElement('cloudly-view-externalregistries')
|
||||||
export class CloudlyViewExternalRegistries extends DeesElement {
|
export class CloudlyViewExternalRegistries extends DeesElement {
|
||||||
@state()
|
@state()
|
||||||
private data: appstate.IDataState = { secretGroups: [], secretBundles: [], externalRegistries: [] } as any;
|
private accessor data: appstate.IDataState = { secretGroups: [], secretBundles: [], externalRegistries: [] } as any;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -115,4 +115,3 @@ export class CloudlyViewExternalRegistries extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
declare global { interface HTMLElementTagNameMap { 'cloudly-view-externalregistries': CloudlyViewExternalRegistries; } }
|
declare global { interface HTMLElementTagNameMap { 'cloudly-view-externalregistries': CloudlyViewExternalRegistries; } }
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import * as appstate from '../../../appstate.js';
|
|||||||
@customElement('cloudly-view-images')
|
@customElement('cloudly-view-images')
|
||||||
export class CloudlyViewImages extends DeesElement {
|
export class CloudlyViewImages extends DeesElement {
|
||||||
@state()
|
@state()
|
||||||
private data: appstate.IDataState = {} as any;
|
private accessor data: appstate.IDataState = {} as any;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -86,8 +86,8 @@ export class CloudlyViewImages extends DeesElement {
|
|||||||
</dees-form>
|
</dees-form>
|
||||||
`,
|
`,
|
||||||
menuOptions: [
|
menuOptions: [
|
||||||
{ name: 'Cancel', iconName: null, action: async (modalArg: any) => { await modalArg.destroy(); } },
|
{ name: 'Cancel', iconName: undefined, 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: '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;
|
'cloudly-view-images': CloudlyViewImages;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import * as appstate from '../../../appstate.js';
|
|||||||
@customElement('cloudly-view-logs')
|
@customElement('cloudly-view-logs')
|
||||||
export class CloudlyViewLogs extends DeesElement {
|
export class CloudlyViewLogs extends DeesElement {
|
||||||
@state()
|
@state()
|
||||||
private data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
|
private accessor data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import * as appstate from '../../../appstate.js';
|
|||||||
@customElement('cloudly-view-mails')
|
@customElement('cloudly-view-mails')
|
||||||
export class CloudlyViewMails extends DeesElement {
|
export class CloudlyViewMails extends DeesElement {
|
||||||
@state()
|
@state()
|
||||||
private data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
|
private accessor data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import * as appstate from '../../../appstate.js';
|
|||||||
@customElement('cloudly-view-overview')
|
@customElement('cloudly-view-overview')
|
||||||
export class CloudlyViewOverview extends DeesElement {
|
export class CloudlyViewOverview extends DeesElement {
|
||||||
@state()
|
@state()
|
||||||
private data: appstate.IDataState = {
|
private accessor data: appstate.IDataState = {
|
||||||
secretGroups: [],
|
secretGroups: [],
|
||||||
secretBundles: [],
|
secretBundles: [],
|
||||||
};
|
};
|
||||||
@@ -68,4 +68,3 @@ declare global {
|
|||||||
'cloudly-view-overview': CloudlyViewOverview;
|
'cloudly-view-overview': CloudlyViewOverview;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import * as appstate from '../../../appstate.js';
|
|||||||
@customElement('cloudly-view-s3')
|
@customElement('cloudly-view-s3')
|
||||||
export class CloudlyViewS3 extends DeesElement {
|
export class CloudlyViewS3 extends DeesElement {
|
||||||
@state()
|
@state()
|
||||||
private data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
|
private accessor data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import * as appstate from '../../../appstate.js';
|
|||||||
@customElement('cloudly-view-secretbundles')
|
@customElement('cloudly-view-secretbundles')
|
||||||
export class CloudlyViewSecretBundles extends DeesElement {
|
export class CloudlyViewSecretBundles extends DeesElement {
|
||||||
@state()
|
@state()
|
||||||
private data: appstate.IDataState = {} as any;
|
private accessor data: appstate.IDataState = {} as any;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -39,7 +39,7 @@ export class CloudlyViewSecretBundles extends DeesElement {
|
|||||||
const secretGroupIds = itemArg.data.includedSecretGroupIds;
|
const secretGroupIds = itemArg.data.includedSecretGroupIds;
|
||||||
let secretGroupNames: string[] = [];
|
let secretGroupNames: string[] = [];
|
||||||
for (const secretGroupId of secretGroupIds) {
|
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); }
|
if (secretGroup) { secretGroupNames.push(secretGroup.data.name); }
|
||||||
}
|
}
|
||||||
return secretGroupNames.join(', ');
|
return secretGroupNames.join(', ');
|
||||||
@@ -73,4 +73,3 @@ export class CloudlyViewSecretBundles extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
declare global { interface HTMLElementTagNameMap { 'cloudly-view-secretbundles': CloudlyViewSecretBundles; } }
|
declare global { interface HTMLElementTagNameMap { 'cloudly-view-secretbundles': CloudlyViewSecretBundles; } }
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import * as appstate from '../../../appstate.js';
|
|||||||
@customElement('cloudly-view-secretsgroups')
|
@customElement('cloudly-view-secretsgroups')
|
||||||
export class CloudlyViewSecretGroups extends DeesElement {
|
export class CloudlyViewSecretGroups extends DeesElement {
|
||||||
@state()
|
@state()
|
||||||
private data: appstate.IDataState = {} as any;
|
private accessor data: appstate.IDataState = {} as any;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -31,7 +31,7 @@ export class CloudlyViewSecretGroups extends DeesElement {
|
|||||||
priority: secretGroup.data.priority,
|
priority: secretGroup.data.priority,
|
||||||
tags: html`<dees-chips .selectionMode=${'none'} .selectableChips=${secretGroup.data.tags}></dees-chips>`,
|
tags: html`<dees-chips .selectionMode=${'none'} .selectableChips=${secretGroup.data.tags}></dees-chips>`,
|
||||||
key: secretGroup.data.key,
|
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=${[
|
.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 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-table>
|
||||||
</dees-form>
|
</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>) => {
|
{ 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; }> = [];
|
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 .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-table>
|
||||||
</dees-form>
|
</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>) => {
|
{ 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, }); } }
|
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; } }
|
declare global { interface HTMLElementTagNameMap { 'cloudly-view-secretsgroups': CloudlyViewSecretGroups; } }
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import * as appstate from '../../../appstate.js';
|
|||||||
@customElement('cloudly-view-services')
|
@customElement('cloudly-view-services')
|
||||||
export class CloudlyViewServices extends DeesElement {
|
export class CloudlyViewServices extends DeesElement {
|
||||||
@state()
|
@state()
|
||||||
private data: appstate.IDataState = {} as any;
|
private accessor data: appstate.IDataState = {} as any;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ import * as appstate from '../../../appstate.js';
|
|||||||
@customElement('cloudly-view-settings')
|
@customElement('cloudly-view-settings')
|
||||||
export class CloudlyViewSettings extends DeesElement {
|
export class CloudlyViewSettings extends DeesElement {
|
||||||
@state()
|
@state()
|
||||||
private settings: plugins.interfaces.data.ICloudlySettingsMasked = {} as any;
|
private accessor settings: plugins.interfaces.data.ICloudlySettingsMasked = {} as any;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private isLoading = false;
|
private accessor isLoading = false;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private testResults: {[key: string]: {success: boolean; message: string}} = {};
|
private accessor testResults: {[key: string]: {success: boolean; message: string}} = {};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|||||||
@@ -18,29 +18,29 @@ import { formatCronFriendly, formatDate, formatDuration } from './utils.js';
|
|||||||
@customElement('cloudly-view-tasks')
|
@customElement('cloudly-view-tasks')
|
||||||
export class CloudlyViewTasks extends DeesElement {
|
export class CloudlyViewTasks extends DeesElement {
|
||||||
@state()
|
@state()
|
||||||
private data: appstate.IDataState = {} as any;
|
private accessor data: appstate.IDataState = {} as any;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private selectedExecution: plugins.interfaces.data.ITaskExecution | null = null;
|
private accessor selectedExecution: plugins.interfaces.data.ITaskExecution | null = null;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private loading = false;
|
private accessor loading = false;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private filterStatus: string = 'all';
|
private accessor filterStatus: string = 'all';
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private searchQuery: string = '';
|
private accessor searchQuery: string = '';
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private categoryFilter: string = 'all';
|
private accessor categoryFilter: string = 'all';
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private autoRefresh: boolean = true;
|
private accessor autoRefresh: boolean = true;
|
||||||
|
|
||||||
private _refreshHandle: any = null;
|
private _refreshHandle: any = null;
|
||||||
@state()
|
@state()
|
||||||
private canceling: Record<string, boolean> = {};
|
private accessor canceling: Record<string, boolean> = {};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -143,7 +143,7 @@ export class CloudlyViewTasks extends DeesElement {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to trigger task:', 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) {
|
} catch (err) {
|
||||||
console.error('Failed to cancel task:', 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')
|
@customElement('cloudly-execution-details')
|
||||||
export class CloudlyExecutionDetails extends DeesElement {
|
export class CloudlyExecutionDetails extends DeesElement {
|
||||||
@property({ type: Object }) execution: any;
|
@property({ type: Object }) accessor execution: any = undefined;
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
@@ -90,4 +90,3 @@ declare global {
|
|||||||
'cloudly-execution-details': CloudlyExecutionDetails;
|
'cloudly-execution-details': CloudlyExecutionDetails;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ import { formatCronFriendly, formatDuration, formatRelativeTime, getCategoryHue,
|
|||||||
|
|
||||||
@customElement('cloudly-task-panel')
|
@customElement('cloudly-task-panel')
|
||||||
export class CloudlyTaskPanel extends DeesElement {
|
export class CloudlyTaskPanel extends DeesElement {
|
||||||
@property({ type: Object }) task: any;
|
@property({ type: Object }) accessor task: any = undefined;
|
||||||
@property({ type: Array }) executions: any[] = [];
|
@property({ type: Array }) accessor executions: any[] = [];
|
||||||
@property({ type: Object }) canceling: Record<string, boolean> = {};
|
@property({ type: Object }) accessor canceling: Record<string, boolean> = {};
|
||||||
|
|
||||||
// Callbacks provided by parent view
|
// Callbacks provided by parent view
|
||||||
@property({ attribute: false }) onRun?: (taskName: string) => void;
|
@property({ attribute: false }) accessor onRun: ((taskName: string) => void) | undefined = undefined;
|
||||||
@property({ attribute: false }) onCancel?: (taskName: string) => void;
|
@property({ attribute: false }) accessor onCancel: ((taskName: string) => void) | undefined = undefined;
|
||||||
@property({ attribute: false }) onOpenDetails?: (execution: any) => void;
|
@property({ attribute: false }) accessor onOpenDetails: ((execution: any) => void) | undefined = undefined;
|
||||||
@property({ attribute: false }) onOpenLogs?: (execution: any) => void;
|
@property({ attribute: false }) accessor onOpenLogs: ((execution: any) => void) | undefined = undefined;
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
@@ -203,4 +203,3 @@ declare global {
|
|||||||
'cloudly-task-panel': CloudlyTaskPanel;
|
'cloudly-task-panel': CloudlyTaskPanel;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -1,10 +1,13 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"experimentalDecorators": true,
|
|
||||||
"useDefineForClassFields": false,
|
"useDefineForClassFields": false,
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "NodeNext",
|
"module": "NodeNext",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
|
"types": [
|
||||||
|
"node"
|
||||||
|
],
|
||||||
|
"ignoreDeprecations": "6.0",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
|
|||||||
Reference in New Issue
Block a user