Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
4e38d2ff43 | |||
e19639c9be | |||
c142519004 | |||
54ef62e7af | |||
83abe37d8c | |||
eefaa55e13 | |||
330797ab1a | |||
4b3b91312b |
1
.serena/.gitignore
vendored
Normal file
1
.serena/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/cache
|
68
.serena/project.yml
Normal file
68
.serena/project.yml
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
|
||||||
|
# * For C, use cpp
|
||||||
|
# * For JavaScript, use typescript
|
||||||
|
# Special requirements:
|
||||||
|
# * csharp: Requires the presence of a .sln file in the project folder.
|
||||||
|
language: typescript
|
||||||
|
|
||||||
|
# whether to use the project's gitignore file to ignore files
|
||||||
|
# Added on 2025-04-07
|
||||||
|
ignore_all_files_in_gitignore: true
|
||||||
|
# list of additional paths to ignore
|
||||||
|
# same syntax as gitignore, so you can use * and **
|
||||||
|
# Was previously called `ignored_dirs`, please update your config if you are using that.
|
||||||
|
# Added (renamed) on 2025-04-07
|
||||||
|
ignored_paths: []
|
||||||
|
|
||||||
|
# whether the project is in read-only mode
|
||||||
|
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||||
|
# Added on 2025-04-18
|
||||||
|
read_only: false
|
||||||
|
|
||||||
|
|
||||||
|
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
||||||
|
# Below is the complete list of tools for convenience.
|
||||||
|
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||||
|
# execute `uv run scripts/print_tool_overview.py`.
|
||||||
|
#
|
||||||
|
# * `activate_project`: Activates a project by name.
|
||||||
|
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||||
|
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||||
|
# * `delete_lines`: Deletes a range of lines within a file.
|
||||||
|
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
||||||
|
# * `execute_shell_command`: Executes a shell command.
|
||||||
|
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
||||||
|
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||||
|
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||||
|
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||||
|
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||||
|
# * `initial_instructions`: Gets the initial instructions for the current project.
|
||||||
|
# Should only be used in settings where the system prompt cannot be set,
|
||||||
|
# e.g. in clients you have no control over, like Claude Desktop.
|
||||||
|
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||||
|
# * `insert_at_line`: Inserts content at a given line in a file.
|
||||||
|
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||||
|
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||||
|
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
||||||
|
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||||
|
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||||
|
# * `read_file`: Reads a file within the project directory.
|
||||||
|
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
||||||
|
# * `remove_project`: Removes a project from the Serena configuration.
|
||||||
|
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
||||||
|
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
||||||
|
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
||||||
|
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||||
|
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
||||||
|
# * `switch_modes`: Activates modes by providing a list of their names
|
||||||
|
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
||||||
|
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
||||||
|
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
||||||
|
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||||
|
excluded_tools: []
|
||||||
|
|
||||||
|
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||||
|
# (contrary to the memories, which are loaded on demand).
|
||||||
|
initial_prompt: ""
|
||||||
|
|
||||||
|
project_name: "cloudly"
|
52
changelog.md
52
changelog.md
@@ -1,5 +1,57 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-09-08 - 5.3.0 - feat(web)
|
||||||
|
Add deployments API typings and web UI improvements: services & deployments management with CRUD and actions
|
||||||
|
|
||||||
|
- Add deployment request interfaces (ts_interfaces/requests/deployment.ts) to define typed API for create/read/update/delete/scale/restart operations.
|
||||||
|
- Extend web app state (ts_web/appstate.ts) to include typed services and deployments, and add actions for create/update/delete of services and deployments.
|
||||||
|
- Enhance web views (ts_web/elements/*): CloudlyViewServices and CloudlyViewDeployments now include richer display, styling, and UI actions (create, edit, deploy, restart, stop, delete).
|
||||||
|
- Fix subscription variable naming in several web components (subecription -> subscription) and improve table display functions to handle missing data safely.
|
||||||
|
- Add .claude/settings.local.json (tooling/permissions) used for local development/test tooling.
|
||||||
|
|
||||||
|
## 2025-09-07 - 5.2.0 - feat(settings)
|
||||||
|
Add runtime settings management, node & baremetal managers, and settings UI
|
||||||
|
|
||||||
|
- Introduce CloudlySettingsManager to store runtime settings in an EasyStore (MongoDB) with API handlers for get/update/clear/test.
|
||||||
|
- Add settings data/interface and typedrequest definitions (ts_interfaces/data/settings.ts, ts_interfaces/requests/settings.ts) and expose via interfaces index.
|
||||||
|
- Add web UI for managing provider credentials and connections (ts_web/elements/cloudly-view-settings.ts) and integrate the Settings view into the dashboard.
|
||||||
|
- Replace the previous ServerManager concept with NodeManager and BaremetalManager: new ClusterNode and BareMetal models and managers (auto-provisioning / Hetzner integration), plus curlfresh moved to node manager.
|
||||||
|
- Update Cluster data shape (servers -> nodes) and adjust related code paths (overview stats, cluster creation and provisioning flows).
|
||||||
|
- Use settingsManager for provider tokens (cloudflareToken, hetznerToken) instead of reading tokens directly from config/env; connector and manager init code updated accordingly.
|
||||||
|
- Add numerous implementations and API handlers to support baremetal/node lifecycle and control (getBaremetalServers, controlBaremetal, getNodeConfig, node provisioning helpers).
|
||||||
|
- Reorder Cloudly startup to initialize MongoDB and settings manager before managers that depend on settings; wire settingsManager into Cloudly class.
|
||||||
|
- Bump package dependency versions for @git.zone/tsdoc, @design.estate/dees-catalog and @push.rocks/taskbuffer in package.json.
|
||||||
|
|
||||||
|
## 2025-09-05 - 5.1.0 - feat(cluster)
|
||||||
|
Add cluster setupMode (manual|hetzner|aws|digitalocean) with conditional Hetzner auto-provisioning; UI and dashboard improvements; dependency upgrades
|
||||||
|
|
||||||
|
- Introduce optional setupMode on cluster configs and requests (ICluster.data.setupMode, createCluster request) to allow 'manual' | 'hetzner' | 'aws' | 'digitalocean'.
|
||||||
|
- ClusterManager: default setupMode to 'manual' when creating clusters and only trigger serverManager.ensureServerInfrastructure() for 'hetzner' clusters.
|
||||||
|
- ServerManager: skip provisioning for clusters not configured with setupMode 'hetzner' and log skipped clusters.
|
||||||
|
- Web UI: add a 'Setup Mode' dropdown when creating a cluster so users can choose auto-provisioning provider; ensure the add-cluster action passes setupMode.
|
||||||
|
- Web UI: dashboard enhancements — add icons to view tabs and replace cluster overview with a stats grid (including total clusters, total servers, images, services, deployments, secret groups/bundles, DNS, DBs, backups, mails, s3). The overview now computes total servers across clusters.
|
||||||
|
- Package dependency bumps (devDependencies and dependencies) to keep libs up-to-date (examples: @git.zone/tsbuild, @git.zone/tstest, @api.global/typedserver, @apiclient.xyz/docker, @design.estate/dees-catalog, @push.rocks/smartlog, @push.rocks/smartrequest, @push.rocks/taskbuffer, etc.).
|
||||||
|
- Add .claude/settings.local.json with local Claude permissions (editor/automation config).
|
||||||
|
|
||||||
|
## 2025-08-18 - 5.0.6 - fix(connector.letsencrypt)
|
||||||
|
Improve Let's Encrypt integration and certificate handling; fix coreflow certificate response; add local assistant permissions config
|
||||||
|
|
||||||
|
- Replace ad-hoc setChallenge/removeChallenge hooks with a DNS-01 handler (smartacme.handlers.Dns01Handler) using Cloudflare to manage ACME DNS challenges.
|
||||||
|
- Add MongoDB-backed certificate manager (smartacme.certmanagers.MongoCertManager) and pass it to SmartAcme as certManager.
|
||||||
|
- Initialize SmartAcme with certManager and challengeHandlers instead of setChallenge/removeChallenge/mongoDescriptor options.
|
||||||
|
- Return certificate object directly from coreflow certificate request handler (avoid createSavableObject) to fix the getCertificateForDomain response payload.
|
||||||
|
- Add .claude/settings.local.json with local assistant/permissions entries to allow specific debugging/automation commands.
|
||||||
|
- Bump commitinfo versions to 5.0.6 and update changelog.
|
||||||
|
|
||||||
|
## 2025-08-18 - 5.0.6 - fix(connector.letsencrypt)
|
||||||
|
Improve Let's Encrypt integration and certificate handling; add local assistant permissions config
|
||||||
|
|
||||||
|
- Replace ad-hoc setChallenge/removeChallenge hooks with a DNS-01 handler using Cloudflare (smartacme.handlers.Dns01Handler) to manage ACME DNS challenges.
|
||||||
|
- Add MongoDB-backed certificate manager (smartacme.certmanagers.MongoCertManager) and pass it to SmartAcme as certManager.
|
||||||
|
- Update SmartAcme initialization to use certManager and challengeHandlers instead of setChallenge/removeChallenge/mongoDescriptor options.
|
||||||
|
- Return certificate object directly from coreflow certificate request handler (avoid createSavableObject), fixing the response payload for getCertificateForDomain.
|
||||||
|
- Add .claude/settings.local.json with local assistant/permissions entries to allow specific debugging/automation commands.
|
||||||
|
|
||||||
## 2025-08-18 - 5.0.5 - fix(coreflow)
|
## 2025-08-18 - 5.0.5 - fix(coreflow)
|
||||||
Fix Coreflow identity lookup and response shape; improve API client tests and bump dependencies
|
Fix Coreflow identity lookup and response shape; improve API client tests and bump dependencies
|
||||||
|
|
||||||
|
20
package.json
20
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/cloudly",
|
"name": "@serve.zone/cloudly",
|
||||||
"version": "5.0.5",
|
"version": "5.3.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"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",
|
||||||
@@ -22,24 +22,24 @@
|
|||||||
"docs": "tsdoc aidoc"
|
"docs": "tsdoc aidoc"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.6.7",
|
"@git.zone/tsbuild": "^2.6.8",
|
||||||
"@git.zone/tsbundle": "^2.5.1",
|
"@git.zone/tsbundle": "^2.5.1",
|
||||||
"@git.zone/tsdoc": "^1.5.1",
|
"@git.zone/tsdoc": "^1.5.2",
|
||||||
"@git.zone/tspublish": "^1.10.3",
|
"@git.zone/tspublish": "^1.10.3",
|
||||||
"@git.zone/tstest": "^2.3.5",
|
"@git.zone/tstest": "^2.3.6",
|
||||||
"@git.zone/tswatch": "^2.2.1",
|
"@git.zone/tswatch": "^2.2.1",
|
||||||
"@types/node": "^22.0.0"
|
"@types/node": "^22.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@api.global/typedrequest": "3.1.10",
|
"@api.global/typedrequest": "3.1.10",
|
||||||
"@api.global/typedrequest-interfaces": "^3.0.19",
|
"@api.global/typedrequest-interfaces": "^3.0.19",
|
||||||
"@api.global/typedserver": "^3.0.77",
|
"@api.global/typedserver": "^3.0.79",
|
||||||
"@api.global/typedsocket": "^3.0.1",
|
"@api.global/typedsocket": "^3.0.1",
|
||||||
"@apiclient.xyz/cloudflare": "^6.4.1",
|
"@apiclient.xyz/cloudflare": "^6.4.1",
|
||||||
"@apiclient.xyz/docker": "^1.3.0",
|
"@apiclient.xyz/docker": "^1.3.5",
|
||||||
"@apiclient.xyz/hetznercloud": "^1.2.0",
|
"@apiclient.xyz/hetznercloud": "^1.2.0",
|
||||||
"@apiclient.xyz/slack": "^3.0.9",
|
"@apiclient.xyz/slack": "^3.0.9",
|
||||||
"@design.estate/dees-catalog": "^1.10.10",
|
"@design.estate/dees-catalog": "^1.11.2",
|
||||||
"@design.estate/dees-domtools": "^2.3.3",
|
"@design.estate/dees-domtools": "^2.3.3",
|
||||||
"@design.estate/dees-element": "^2.1.2",
|
"@design.estate/dees-element": "^2.1.2",
|
||||||
"@git.zone/tsrun": "^1.3.3",
|
"@git.zone/tsrun": "^1.3.3",
|
||||||
@@ -59,19 +59,19 @@
|
|||||||
"@push.rocks/smartguard": "^3.1.0",
|
"@push.rocks/smartguard": "^3.1.0",
|
||||||
"@push.rocks/smartjson": "^5.0.19",
|
"@push.rocks/smartjson": "^5.0.19",
|
||||||
"@push.rocks/smartjwt": "^2.2.1",
|
"@push.rocks/smartjwt": "^2.2.1",
|
||||||
"@push.rocks/smartlog": "^3.1.8",
|
"@push.rocks/smartlog": "^3.1.9",
|
||||||
"@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.3",
|
||||||
"@push.rocks/smartrequest": "^4.2.2",
|
"@push.rocks/smartrequest": "^4.3.1",
|
||||||
"@push.rocks/smartrx": "^3.0.10",
|
"@push.rocks/smartrx": "^3.0.10",
|
||||||
"@push.rocks/smartssh": "^2.0.1",
|
"@push.rocks/smartssh": "^2.0.1",
|
||||||
"@push.rocks/smartstate": "^2.0.26",
|
"@push.rocks/smartstate": "^2.0.26",
|
||||||
"@push.rocks/smartstream": "^3.2.5",
|
"@push.rocks/smartstream": "^3.2.5",
|
||||||
"@push.rocks/smartstring": "^4.0.15",
|
"@push.rocks/smartstring": "^4.0.15",
|
||||||
"@push.rocks/smartunique": "^3.0.9",
|
"@push.rocks/smartunique": "^3.0.9",
|
||||||
"@push.rocks/taskbuffer": "^3.0.2",
|
"@push.rocks/taskbuffer": "^3.4.0",
|
||||||
"@push.rocks/webjwt": "^1.0.9",
|
"@push.rocks/webjwt": "^1.0.9",
|
||||||
"@tsclass/tsclass": "^9.2.0"
|
"@tsclass/tsclass": "^9.2.0"
|
||||||
},
|
},
|
||||||
|
717
pnpm-lock.yaml
generated
717
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/cloudly',
|
name: '@serve.zone/cloudly',
|
||||||
version: '5.0.5',
|
version: '5.3.0',
|
||||||
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.'
|
||||||
}
|
}
|
||||||
|
@@ -18,12 +18,14 @@ import { CloudlyCoreflowManager } from './manager.coreflow/coreflowmanager.js';
|
|||||||
import { ClusterManager } from './manager.cluster/classes.clustermanager.js';
|
import { ClusterManager } from './manager.cluster/classes.clustermanager.js';
|
||||||
import { CloudlyTaskmanager } from './manager.task/taskmanager.js';
|
import { CloudlyTaskmanager } from './manager.task/taskmanager.js';
|
||||||
import { CloudlySecretManager } from './manager.secret/classes.secretmanager.js';
|
import { CloudlySecretManager } from './manager.secret/classes.secretmanager.js';
|
||||||
import { CloudlyServerManager } from './manager.server/classes.servermanager.js';
|
import { CloudlyNodeManager } from './manager.node/classes.nodemanager.js';
|
||||||
|
import { CloudlyBaremetalManager } from './manager.baremetal/classes.baremetalmanager.js';
|
||||||
import { ExternalApiManager } from './manager.status/statusmanager.js';
|
import { ExternalApiManager } from './manager.status/statusmanager.js';
|
||||||
import { ExternalRegistryManager } from './manager.externalregistry/index.js';
|
import { ExternalRegistryManager } from './manager.externalregistry/index.js';
|
||||||
import { ImageManager } from './manager.image/classes.imagemanager.js';
|
import { ImageManager } from './manager.image/classes.imagemanager.js';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
import { CloudlyAuthManager } from './manager.auth/classes.authmanager.js';
|
import { CloudlyAuthManager } from './manager.auth/classes.authmanager.js';
|
||||||
|
import { CloudlySettingsManager } from './manager.settings/classes.settingsmanager.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cloudly class can be used to instantiate a cloudly server.
|
* Cloudly class can be used to instantiate a cloudly server.
|
||||||
@@ -52,13 +54,15 @@ export class Cloudly {
|
|||||||
// managers
|
// managers
|
||||||
public authManager: CloudlyAuthManager;
|
public authManager: CloudlyAuthManager;
|
||||||
public secretManager: CloudlySecretManager;
|
public secretManager: CloudlySecretManager;
|
||||||
|
public settingsManager: CloudlySettingsManager;
|
||||||
public clusterManager: ClusterManager;
|
public clusterManager: ClusterManager;
|
||||||
public coreflowManager: CloudlyCoreflowManager;
|
public coreflowManager: CloudlyCoreflowManager;
|
||||||
public externalApiManager: ExternalApiManager;
|
public externalApiManager: ExternalApiManager;
|
||||||
public externalRegistryManager: ExternalRegistryManager;
|
public externalRegistryManager: ExternalRegistryManager;
|
||||||
public imageManager: ImageManager;
|
public imageManager: ImageManager;
|
||||||
public taskManager: CloudlyTaskmanager;
|
public taskManager: CloudlyTaskmanager;
|
||||||
public serverManager: CloudlyServerManager;
|
public nodeManager: CloudlyNodeManager;
|
||||||
|
public baremetalManager: CloudlyBaremetalManager;
|
||||||
|
|
||||||
private readyDeferred = new plugins.smartpromise.Deferred();
|
private readyDeferred = new plugins.smartpromise.Deferred();
|
||||||
|
|
||||||
@@ -79,6 +83,7 @@ export class Cloudly {
|
|||||||
|
|
||||||
// managers
|
// managers
|
||||||
this.authManager = new CloudlyAuthManager(this);
|
this.authManager = new CloudlyAuthManager(this);
|
||||||
|
this.settingsManager = new CloudlySettingsManager(this);
|
||||||
this.clusterManager = new ClusterManager(this);
|
this.clusterManager = new ClusterManager(this);
|
||||||
this.coreflowManager = new CloudlyCoreflowManager(this);
|
this.coreflowManager = new CloudlyCoreflowManager(this);
|
||||||
this.externalApiManager = new ExternalApiManager(this);
|
this.externalApiManager = new ExternalApiManager(this);
|
||||||
@@ -86,7 +91,8 @@ export class Cloudly {
|
|||||||
this.imageManager = new ImageManager(this);
|
this.imageManager = new ImageManager(this);
|
||||||
this.taskManager = new CloudlyTaskmanager(this);
|
this.taskManager = new CloudlyTaskmanager(this);
|
||||||
this.secretManager = new CloudlySecretManager(this);
|
this.secretManager = new CloudlySecretManager(this);
|
||||||
this.serverManager = new CloudlyServerManager(this);
|
this.nodeManager = new CloudlyNodeManager(this);
|
||||||
|
this.baremetalManager = new CloudlyBaremetalManager(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -97,13 +103,18 @@ export class Cloudly {
|
|||||||
// config
|
// config
|
||||||
await this.config.init(this.configOptions);
|
await this.config.init(this.configOptions);
|
||||||
|
|
||||||
|
// database (data comes from config)
|
||||||
|
await this.mongodbConnector.init();
|
||||||
|
|
||||||
|
// settings (are stored in db)
|
||||||
|
await this.settingsManager.init();
|
||||||
|
|
||||||
// manageers
|
// manageers
|
||||||
await this.authManager.start();
|
await this.authManager.start();
|
||||||
await this.secretManager.start();
|
await this.secretManager.start();
|
||||||
await this.serverManager.start();
|
await this.nodeManager.start();
|
||||||
|
await this.baremetalManager.start();
|
||||||
// connectors
|
|
||||||
await this.mongodbConnector.init();
|
|
||||||
await this.cloudflareConnector.init();
|
await this.cloudflareConnector.init();
|
||||||
await this.letsencryptConnector.init();
|
await this.letsencryptConnector.init();
|
||||||
await this.clusterManager.init();
|
await this.clusterManager.init();
|
||||||
|
@@ -20,10 +20,8 @@ export class CloudlyConfig {
|
|||||||
await plugins.npmextra.AppData.createAndInit<plugins.servezoneInterfaces.data.ICloudlyConfig>(
|
await plugins.npmextra.AppData.createAndInit<plugins.servezoneInterfaces.data.ICloudlyConfig>(
|
||||||
{
|
{
|
||||||
envMapping: {
|
envMapping: {
|
||||||
cfToken: 'CF_TOKEN',
|
|
||||||
environment: 'SERVEZONE_ENVIRONMENT' as 'production' | 'integration',
|
environment: 'SERVEZONE_ENVIRONMENT' as 'production' | 'integration',
|
||||||
letsEncryptEmail: 'hard:domains@lossless.org',
|
letsEncryptEmail: 'hard:domains@lossless.org',
|
||||||
hetznerToken: 'HETZNER_API_TOKEN',
|
|
||||||
letsEncryptPrivateKey: null,
|
letsEncryptPrivateKey: null,
|
||||||
publicUrl: 'SERVEZONE_URL',
|
publicUrl: 'SERVEZONE_URL',
|
||||||
publicPort: 'SERVEZONE_PORT',
|
publicPort: 'SERVEZONE_PORT',
|
||||||
@@ -46,8 +44,6 @@ export class CloudlyConfig {
|
|||||||
servezoneAdminaccount: 'SERVEZONE_ADMINACCOUNT',
|
servezoneAdminaccount: 'SERVEZONE_ADMINACCOUNT',
|
||||||
},
|
},
|
||||||
requiredKeys: [
|
requiredKeys: [
|
||||||
'cfToken',
|
|
||||||
'hetznerToken',
|
|
||||||
'letsEncryptEmail',
|
'letsEncryptEmail',
|
||||||
'publicUrl',
|
'publicUrl',
|
||||||
'publicPort',
|
'publicPort',
|
||||||
|
@@ -95,7 +95,7 @@ export class CloudlyServer {
|
|||||||
this.typedServer.typedrouter.addTypedRouter(this.typedrouter);
|
this.typedServer.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
this.typedServer.server.addRoute(
|
this.typedServer.server.addRoute(
|
||||||
'/curlfresh/:scriptname',
|
'/curlfresh/:scriptname',
|
||||||
this.cloudlyRef.serverManager.curlfreshInstance.handler,
|
this.cloudlyRef.nodeManager.curlfreshInstance.handler,
|
||||||
);
|
);
|
||||||
await this.typedServer.start();
|
await this.typedServer.start();
|
||||||
}
|
}
|
||||||
|
@@ -14,6 +14,13 @@ export class CloudflareConnector {
|
|||||||
|
|
||||||
// init the instance
|
// init the instance
|
||||||
public async init() {
|
public async init() {
|
||||||
this.cloudflare = new plugins.cloudflare.CloudflareAccount(this.cloudlyRef.config.data.cfToken);
|
const cloudflareToken = await this.cloudlyRef.settingsManager.getSetting('cloudflareToken');
|
||||||
|
|
||||||
|
if (!cloudflareToken) {
|
||||||
|
console.log('warn', 'No Cloudflare token configured in settings. Cloudflare features will be disabled.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cloudflare = new plugins.cloudflare.CloudflareAccount(cloudflareToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -18,21 +18,22 @@ export class LetsencryptConnector {
|
|||||||
* inits letsencrypt
|
* inits letsencrypt
|
||||||
*/
|
*/
|
||||||
public async init() {
|
public async init() {
|
||||||
|
// Create DNS-01 challenge handler using Cloudflare
|
||||||
|
const dnsHandler = new plugins.smartacme.handlers.Dns01Handler(
|
||||||
|
this.cloudlyRef.cloudflareConnector.cloudflare
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create MongoDB certificate manager
|
||||||
|
const certManager = new plugins.smartacme.certmanagers.MongoCertManager(
|
||||||
|
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,
|
||||||
setChallenge: async (dnsChallenge) => {
|
certManager: certManager,
|
||||||
await this.cloudlyRef.cloudflareConnector.cloudflare.convenience.acmeSetDnsChallenge(
|
challengeHandlers: [dnsHandler],
|
||||||
dnsChallenge,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
removeChallenge: async (dnsChallenge) => {
|
|
||||||
await this.cloudlyRef.cloudflareConnector.cloudflare.convenience.acmeRemoveDnsChallenge(
|
|
||||||
dnsChallenge,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
mongoDescriptor: this.cloudlyRef.config.data.mongoDescriptor,
|
|
||||||
});
|
});
|
||||||
await this.smartacme.start().catch((err) => {
|
await this.smartacme.start().catch((err) => {
|
||||||
console.error('error in init', err);
|
console.error('error in init', err);
|
||||||
|
104
ts/manager.baremetal/classes.baremetal.ts
Normal file
104
ts/manager.baremetal/classes.baremetal.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BareMetal represents an actual physical server
|
||||||
|
*/
|
||||||
|
@plugins.smartdata.Manager()
|
||||||
|
export class BareMetal extends plugins.smartdata.SmartDataDbDoc<
|
||||||
|
BareMetal,
|
||||||
|
plugins.servezoneInterfaces.data.IBareMetal
|
||||||
|
> {
|
||||||
|
// STATIC
|
||||||
|
public static async createFromHetznerServer(
|
||||||
|
hetznerServerArg: plugins.hetznercloud.HetznerServer,
|
||||||
|
) {
|
||||||
|
const newBareMetal = new BareMetal();
|
||||||
|
newBareMetal.id = plugins.smartunique.shortId(8);
|
||||||
|
const data: plugins.servezoneInterfaces.data.IBareMetal['data'] = {
|
||||||
|
hostname: hetznerServerArg.data.name,
|
||||||
|
primaryIp: hetznerServerArg.data.public_net.ipv4.ip,
|
||||||
|
provider: 'hetzner',
|
||||||
|
location: hetznerServerArg.data.datacenter.name,
|
||||||
|
specs: {
|
||||||
|
cpuModel: hetznerServerArg.data.server_type.cpu_type,
|
||||||
|
cpuCores: hetznerServerArg.data.server_type.cores,
|
||||||
|
memoryGB: hetznerServerArg.data.server_type.memory,
|
||||||
|
storageGB: hetznerServerArg.data.server_type.disk,
|
||||||
|
storageType: 'nvme',
|
||||||
|
},
|
||||||
|
powerState: hetznerServerArg.data.status === 'running' ? 'on' : 'off',
|
||||||
|
osInfo: {
|
||||||
|
name: 'Debian',
|
||||||
|
version: '12',
|
||||||
|
},
|
||||||
|
assignedNodeIds: [],
|
||||||
|
providerMetadata: {
|
||||||
|
hetznerServerId: hetznerServerArg.data.id,
|
||||||
|
hetznerServerName: hetznerServerArg.data.name,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Object.assign(newBareMetal, { data });
|
||||||
|
await newBareMetal.save();
|
||||||
|
return newBareMetal;
|
||||||
|
}
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
|
@plugins.smartdata.unI()
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public data: plugins.servezoneInterfaces.data.IBareMetal['data'];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async assignNode(nodeId: string) {
|
||||||
|
if (!this.data.assignedNodeIds.includes(nodeId)) {
|
||||||
|
this.data.assignedNodeIds.push(nodeId);
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async removeNode(nodeId: string) {
|
||||||
|
this.data.assignedNodeIds = this.data.assignedNodeIds.filter(id => id !== nodeId);
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updatePowerState(state: 'on' | 'off' | 'unknown') {
|
||||||
|
this.data.powerState = state;
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async powerOn(): Promise<boolean> {
|
||||||
|
// TODO: Implement IPMI power on
|
||||||
|
if (this.data.ipmiAddress && this.data.ipmiCredentials) {
|
||||||
|
// Implement IPMI power on command
|
||||||
|
console.log(`Powering on BareMetal ${this.id} via IPMI`);
|
||||||
|
await this.updatePowerState('on');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async powerOff(): Promise<boolean> {
|
||||||
|
// TODO: Implement IPMI power off
|
||||||
|
if (this.data.ipmiAddress && this.data.ipmiCredentials) {
|
||||||
|
// Implement IPMI power off command
|
||||||
|
console.log(`Powering off BareMetal ${this.id} via IPMI`);
|
||||||
|
await this.updatePowerState('off');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async reset(): Promise<boolean> {
|
||||||
|
// TODO: Implement IPMI reset
|
||||||
|
if (this.data.ipmiAddress && this.data.ipmiCredentials) {
|
||||||
|
// Implement IPMI reset command
|
||||||
|
console.log(`Resetting BareMetal ${this.id} via IPMI`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
176
ts/manager.baremetal/classes.baremetalmanager.ts
Normal file
176
ts/manager.baremetal/classes.baremetalmanager.ts
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import { Cloudly } from '../classes.cloudly.js';
|
||||||
|
import { BareMetal } from './classes.baremetal.js';
|
||||||
|
import { logger } from '../logger.js';
|
||||||
|
|
||||||
|
export class CloudlyBaremetalManager {
|
||||||
|
public cloudlyRef: Cloudly;
|
||||||
|
public typedRouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
|
||||||
|
public hetznerAccount: plugins.hetznercloud.HetznerAccount;
|
||||||
|
|
||||||
|
public get db() {
|
||||||
|
return this.cloudlyRef.mongodbConnector.smartdataDb;
|
||||||
|
}
|
||||||
|
public CBareMetal = plugins.smartdata.setDefaultManagerForDoc(this, BareMetal);
|
||||||
|
|
||||||
|
constructor(cloudlyRefArg: Cloudly) {
|
||||||
|
this.cloudlyRef = cloudlyRefArg;
|
||||||
|
this.cloudlyRef.typedrouter.addTypedRouter(this.typedRouter);
|
||||||
|
|
||||||
|
// API endpoint to get baremetal servers
|
||||||
|
this.typedRouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.baremetal.IRequest_Any_Cloudly_GetBaremetalServers>(
|
||||||
|
'getBaremetalServers',
|
||||||
|
async (requestData) => {
|
||||||
|
const baremetals = await this.getAllBaremetals();
|
||||||
|
return {
|
||||||
|
baremetals: await Promise.all(
|
||||||
|
baremetals.map((baremetal) => baremetal.createSavableObject())
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// API endpoint to control baremetal via IPMI
|
||||||
|
this.typedRouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.baremetal.IRequest_Any_Cloudly_ControlBaremetal>(
|
||||||
|
'controlBaremetal',
|
||||||
|
async (requestData) => {
|
||||||
|
const baremetal = await this.CBareMetal.getInstance({
|
||||||
|
id: requestData.baremetalId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!baremetal) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'BareMetal not found',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let success = false;
|
||||||
|
switch (requestData.action) {
|
||||||
|
case 'powerOn':
|
||||||
|
success = await baremetal.powerOn();
|
||||||
|
break;
|
||||||
|
case 'powerOff':
|
||||||
|
success = await baremetal.powerOff();
|
||||||
|
break;
|
||||||
|
case 'reset':
|
||||||
|
success = await baremetal.reset();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success,
|
||||||
|
message: success ? `Action ${requestData.action} completed` : `Action ${requestData.action} failed`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async start() {
|
||||||
|
const hetznerToken = await this.cloudlyRef.settingsManager.getSetting('hetznerToken');
|
||||||
|
|
||||||
|
if (hetznerToken) {
|
||||||
|
this.hetznerAccount = new plugins.hetznercloud.HetznerAccount(hetznerToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('info', 'BareMetal manager started');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stop() {
|
||||||
|
logger.log('info', 'BareMetal manager stopped');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all baremetal servers
|
||||||
|
*/
|
||||||
|
public async getAllBaremetals(): Promise<BareMetal[]> {
|
||||||
|
const baremetals = await this.CBareMetal.getInstances({});
|
||||||
|
return baremetals;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get baremetal by ID
|
||||||
|
*/
|
||||||
|
public async getBaremetalById(id: string): Promise<BareMetal | null> {
|
||||||
|
const baremetal = await this.CBareMetal.getInstance({
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
return baremetal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get baremetals by provider
|
||||||
|
*/
|
||||||
|
public async getBaremetalsByProvider(provider: 'hetzner' | 'aws' | 'digitalocean' | 'onpremise'): Promise<BareMetal[]> {
|
||||||
|
const baremetals = await this.CBareMetal.getInstances({
|
||||||
|
data: {
|
||||||
|
provider,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return baremetals;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create baremetal from Hetzner server
|
||||||
|
*/
|
||||||
|
public async createBaremetalFromHetznerServer(hetznerServer: plugins.hetznercloud.HetznerServer): Promise<BareMetal> {
|
||||||
|
// Check if baremetal already exists for this Hetzner server
|
||||||
|
const existingBaremetals = await this.CBareMetal.getInstances({});
|
||||||
|
for (const baremetal of existingBaremetals) {
|
||||||
|
if (baremetal.data.providerMetadata?.hetznerServerId === hetznerServer.data.id) {
|
||||||
|
logger.log('info', `BareMetal already exists for Hetzner server ${hetznerServer.data.id}`);
|
||||||
|
return baremetal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new baremetal
|
||||||
|
const newBaremetal = await BareMetal.createFromHetznerServer(hetznerServer);
|
||||||
|
logger.log('success', `Created new BareMetal ${newBaremetal.id} from Hetzner server ${hetznerServer.data.id}`);
|
||||||
|
return newBaremetal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync baremetals with Hetzner
|
||||||
|
*/
|
||||||
|
public async syncWithHetzner() {
|
||||||
|
if (!this.hetznerAccount) {
|
||||||
|
logger.log('warn', 'Cannot sync with Hetzner - no account configured');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hetznerServers = await this.hetznerAccount.getServers();
|
||||||
|
|
||||||
|
for (const hetznerServer of hetznerServers) {
|
||||||
|
await this.createBaremetalFromHetznerServer(hetznerServer);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('success', `Synced ${hetznerServers.length} servers from Hetzner`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provision a new baremetal server
|
||||||
|
*/
|
||||||
|
public async provisionBaremetal(options: {
|
||||||
|
provider: 'hetzner' | 'aws' | 'digitalocean';
|
||||||
|
location: any; // TODO: Import proper type from hetznercloud when available
|
||||||
|
type: any; // TODO: Import proper type from hetznercloud when available
|
||||||
|
}): Promise<BareMetal> {
|
||||||
|
if (options.provider === 'hetzner' && this.hetznerAccount) {
|
||||||
|
const hetznerServer = await this.hetznerAccount.createServer({
|
||||||
|
name: plugins.smartunique.uniSimple('baremetal'),
|
||||||
|
location: options.location,
|
||||||
|
type: options.type,
|
||||||
|
});
|
||||||
|
|
||||||
|
const baremetal = await this.createBaremetalFromHetznerServer(hetznerServer);
|
||||||
|
return baremetal;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Provider ${options.provider} not supported or not configured`);
|
||||||
|
}
|
||||||
|
}
|
@@ -24,19 +24,26 @@ export class ClusterManager {
|
|||||||
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) => {
|
||||||
// TODO: guards
|
// TODO: guards
|
||||||
|
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: null, // this is created by the createCluster method
|
||||||
name: dataArg.clusterName,
|
name: dataArg.clusterName,
|
||||||
|
setupMode: setupMode,
|
||||||
acmeInfo: null,
|
acmeInfo: null,
|
||||||
cloudlyUrl: `https://${this.cloudlyRef.config.data.publicUrl}:${this.cloudlyRef.config.data.publicPort}/`,
|
cloudlyUrl: `https://${this.cloudlyRef.config.data.publicUrl}:${this.cloudlyRef.config.data.publicPort}/`,
|
||||||
servers: [],
|
nodes: [],
|
||||||
sshKeys: [],
|
sshKeys: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
console.log(await cluster.createSavableObject());
|
console.log(await cluster.createSavableObject());
|
||||||
this.cloudlyRef.serverManager.ensureServerInfrastructure();
|
|
||||||
|
// Only auto-provision servers if setupMode is 'hetzner'
|
||||||
|
if (setupMode === 'hetzner') {
|
||||||
|
this.cloudlyRef.nodeManager.ensureNodeInfrastructure();
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cluster: await cluster.createSavableObject(),
|
cluster: await cluster.createSavableObject(),
|
||||||
};
|
};
|
||||||
|
@@ -92,7 +92,7 @@ export class CloudlyCoreflowManager {
|
|||||||
);
|
);
|
||||||
console.log(`got certificate ready for reponse ${dataArg.domainName}`);
|
console.log(`got certificate ready for reponse ${dataArg.domainName}`);
|
||||||
return {
|
return {
|
||||||
certificate: await cert.createSavableObject(),
|
certificate: cert,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
61
ts/manager.node/classes.clusternode.ts
Normal file
61
ts/manager.node/classes.clusternode.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ClusterNode represents a logical node participating in a cluster
|
||||||
|
*/
|
||||||
|
@plugins.smartdata.Manager()
|
||||||
|
export class ClusterNode extends plugins.smartdata.SmartDataDbDoc<
|
||||||
|
ClusterNode,
|
||||||
|
plugins.servezoneInterfaces.data.IClusterNode
|
||||||
|
> {
|
||||||
|
// STATIC
|
||||||
|
public static async createFromHetznerServer(
|
||||||
|
hetznerServerArg: plugins.hetznercloud.HetznerServer,
|
||||||
|
clusterId: string,
|
||||||
|
baremetalId: string,
|
||||||
|
) {
|
||||||
|
const newNode = new ClusterNode();
|
||||||
|
newNode.id = plugins.smartunique.shortId(8);
|
||||||
|
const data: plugins.servezoneInterfaces.data.IClusterNode['data'] = {
|
||||||
|
clusterId: clusterId,
|
||||||
|
baremetalId: baremetalId,
|
||||||
|
nodeType: 'baremetal',
|
||||||
|
status: 'initializing',
|
||||||
|
role: 'worker',
|
||||||
|
joinedAt: Date.now(),
|
||||||
|
lastHealthCheck: Date.now(),
|
||||||
|
sshKeys: [],
|
||||||
|
requiredDebianPackages: [],
|
||||||
|
};
|
||||||
|
Object.assign(newNode, { data });
|
||||||
|
await newNode.save();
|
||||||
|
return newNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
|
@plugins.smartdata.unI()
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public data: plugins.servezoneInterfaces.data.IClusterNode['data'];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getDeployments(): Promise<plugins.servezoneInterfaces.data.IDeployment[]> {
|
||||||
|
// TODO: Implement getting deployments for this node
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateMetrics(metrics: plugins.servezoneInterfaces.data.IClusterNodeMetrics) {
|
||||||
|
this.data.metrics = metrics;
|
||||||
|
this.data.lastHealthCheck = Date.now();
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateStatus(status: plugins.servezoneInterfaces.data.IClusterNode['data']['status']) {
|
||||||
|
this.data.status = status;
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
|
}
|
@@ -1,6 +1,6 @@
|
|||||||
import { logger } from '../logger.js';
|
import { logger } from '../logger.js';
|
||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import type { CloudlyServerManager } from './classes.servermanager.js';
|
import type { CloudlyNodeManager } from './classes.nodemanager.js';
|
||||||
|
|
||||||
export class CurlFresh {
|
export class CurlFresh {
|
||||||
public optionsArg = {
|
public optionsArg = {
|
||||||
@@ -45,7 +45,7 @@ bash -c "spark installdaemon"
|
|||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
|
|
||||||
public serverManagerRef: CloudlyServerManager;
|
public nodeManagerRef: CloudlyNodeManager;
|
||||||
public curlFreshRoute: plugins.typedserver.servertools.Route;
|
public curlFreshRoute: plugins.typedserver.servertools.Route;
|
||||||
public handler = new plugins.typedserver.servertools.Handler('ALL', async (req, res) => {
|
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 :)');
|
||||||
@@ -62,12 +62,12 @@ bash -c "spark installdaemon"
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
constructor(serverManagerRefArg: CloudlyServerManager) {
|
constructor(nodeManagerRefArg: CloudlyNodeManager) {
|
||||||
this.serverManagerRef = serverManagerRefArg;
|
this.nodeManagerRef = nodeManagerRefArg;
|
||||||
}
|
}
|
||||||
public async getServerUserData(): Promise<string> {
|
public async getServerUserData(): Promise<string> {
|
||||||
const sslMode =
|
const sslMode =
|
||||||
await this.serverManagerRef.cloudlyRef.config.appData.waitForAndGetKey('sslMode');
|
await this.nodeManagerRef.cloudlyRef.config.appData.waitForAndGetKey('sslMode');
|
||||||
let protocol: 'http' | 'https';
|
let protocol: 'http' | 'https';
|
||||||
if (sslMode === 'none') {
|
if (sslMode === 'none') {
|
||||||
protocol = 'http';
|
protocol = 'http';
|
||||||
@@ -76,9 +76,9 @@ bash -c "spark installdaemon"
|
|||||||
}
|
}
|
||||||
|
|
||||||
const domain =
|
const domain =
|
||||||
await this.serverManagerRef.cloudlyRef.config.appData.waitForAndGetKey('publicUrl');
|
await this.nodeManagerRef.cloudlyRef.config.appData.waitForAndGetKey('publicUrl');
|
||||||
const port =
|
const port =
|
||||||
await this.serverManagerRef.cloudlyRef.config.appData.waitForAndGetKey('publicPort');
|
await this.nodeManagerRef.cloudlyRef.config.appData.waitForAndGetKey('publicPort');
|
||||||
|
|
||||||
const serverUserData = `#cloud-config
|
const serverUserData = `#cloud-config
|
||||||
runcmd:
|
runcmd:
|
131
ts/manager.node/classes.nodemanager.ts
Normal file
131
ts/manager.node/classes.nodemanager.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import { Cloudly } from '../classes.cloudly.js';
|
||||||
|
import { Cluster } from '../manager.cluster/classes.cluster.js';
|
||||||
|
import { ClusterNode } from './classes.clusternode.js';
|
||||||
|
import { CurlFresh } from './classes.curlfresh.js';
|
||||||
|
|
||||||
|
export class CloudlyNodeManager {
|
||||||
|
public cloudlyRef: Cloudly;
|
||||||
|
public typedRouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
public curlfreshInstance = new CurlFresh(this);
|
||||||
|
|
||||||
|
public hetznerAccount: plugins.hetznercloud.HetznerAccount;
|
||||||
|
|
||||||
|
public get db() {
|
||||||
|
return this.cloudlyRef.mongodbConnector.smartdataDb;
|
||||||
|
}
|
||||||
|
public CClusterNode = plugins.smartdata.setDefaultManagerForDoc(this, ClusterNode);
|
||||||
|
|
||||||
|
constructor(cloudlyRefArg: Cloudly) {
|
||||||
|
this.cloudlyRef = cloudlyRefArg;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* is used be serverconfig module on the node to get the actual node config
|
||||||
|
*/
|
||||||
|
this.typedRouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.config.IRequest_Any_Cloudly_GetNodeConfig>(
|
||||||
|
'getNodeConfig',
|
||||||
|
async (requestData) => {
|
||||||
|
const nodeId = requestData.nodeId;
|
||||||
|
const node = await this.CClusterNode.getInstance({
|
||||||
|
id: nodeId,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
configData: await node.createSavableObject(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async start() {
|
||||||
|
const hetznerToken = await this.cloudlyRef.settingsManager.getSetting('hetznerToken');
|
||||||
|
|
||||||
|
if (!hetznerToken) {
|
||||||
|
console.log('warn', 'No Hetzner token configured in settings. Hetzner features will be disabled.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hetznerAccount = new plugins.hetznercloud.HetznerAccount(hetznerToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stop() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* creates the node infrastructure on hetzner
|
||||||
|
* ensures that there are exactly the resources that are needed
|
||||||
|
* no more, no less
|
||||||
|
*/
|
||||||
|
public async ensureNodeInfrastructure() {
|
||||||
|
// get all clusters
|
||||||
|
const allClusters = await this.cloudlyRef.clusterManager.getAllClusters();
|
||||||
|
for (const cluster of allClusters) {
|
||||||
|
// Skip clusters that are not set up for Hetzner auto-provisioning
|
||||||
|
if (cluster.data.setupMode !== 'hetzner') {
|
||||||
|
console.log(`Skipping node provisioning for cluster ${cluster.id} - setupMode is ${cluster.data.setupMode || 'manual'}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get existing nodes
|
||||||
|
const nodes = await this.getNodesByCluster(cluster);
|
||||||
|
|
||||||
|
// if there is no node, create one
|
||||||
|
if (nodes.length === 0) {
|
||||||
|
const hetznerServer = await this.hetznerAccount.createServer({
|
||||||
|
name: plugins.smartunique.uniSimple('node'),
|
||||||
|
location: 'nbg1',
|
||||||
|
type: 'cpx41',
|
||||||
|
labels: {
|
||||||
|
clusterId: cluster.id,
|
||||||
|
priority: '1',
|
||||||
|
},
|
||||||
|
userData: await this.curlfreshInstance.getServerUserData(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// First create BareMetal record
|
||||||
|
const baremetal = await this.cloudlyRef.baremetalManager.createBaremetalFromHetznerServer(hetznerServer);
|
||||||
|
|
||||||
|
const newNode = await ClusterNode.createFromHetznerServer(hetznerServer, cluster.id, baremetal.id);
|
||||||
|
await baremetal.assignNode(newNode.id);
|
||||||
|
console.log(`cluster created new node for cluster ${cluster.id}`);
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
`cluster ${cluster.id} already has nodes. Making sure that they actually exist in the real world...`,
|
||||||
|
);
|
||||||
|
// if there is a node, make sure that it exists
|
||||||
|
for (const node of nodes) {
|
||||||
|
const hetznerServers = await this.hetznerAccount.getServersByLabel({
|
||||||
|
clusterId: cluster.id,
|
||||||
|
});
|
||||||
|
if (!hetznerServers || hetznerServers.length === 0) {
|
||||||
|
console.log(`node ${node.id} does not exist in the real world. Creating it now...`);
|
||||||
|
const hetznerServer = await this.hetznerAccount.createServer({
|
||||||
|
name: plugins.smartunique.uniSimple('node'),
|
||||||
|
location: 'nbg1',
|
||||||
|
type: 'cpx41',
|
||||||
|
labels: {
|
||||||
|
clusterId: cluster.id,
|
||||||
|
priority: '1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// First create BareMetal record
|
||||||
|
const baremetal = await this.cloudlyRef.baremetalManager.createBaremetalFromHetznerServer(hetznerServer);
|
||||||
|
|
||||||
|
const newNode = await ClusterNode.createFromHetznerServer(hetznerServer, cluster.id, baremetal.id);
|
||||||
|
await baremetal.assignNode(newNode.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getNodesByCluster(clusterArg: Cluster) {
|
||||||
|
const results = await this.CClusterNode.getInstances({
|
||||||
|
data: {
|
||||||
|
clusterId: clusterArg.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,42 +0,0 @@
|
|||||||
import * as plugins from '../plugins.js';
|
|
||||||
|
|
||||||
/*
|
|
||||||
* cluster defines a swarmkit cluster
|
|
||||||
*/
|
|
||||||
@plugins.smartdata.Manager()
|
|
||||||
export class Server extends plugins.smartdata.SmartDataDbDoc<
|
|
||||||
Server,
|
|
||||||
plugins.servezoneInterfaces.data.IServer
|
|
||||||
> {
|
|
||||||
// STATIC
|
|
||||||
public static async createFromHetznerServer(
|
|
||||||
hetznerServerArg: plugins.hetznercloud.HetznerServer,
|
|
||||||
) {
|
|
||||||
const newServer = new Server();
|
|
||||||
newServer.id = plugins.smartunique.shortId(8);
|
|
||||||
const data: plugins.servezoneInterfaces.data.IServer['data'] = {
|
|
||||||
assignedClusterId: hetznerServerArg.data.labels.clusterId,
|
|
||||||
requiredDebianPackages: [],
|
|
||||||
sshKeys: [],
|
|
||||||
type: 'hetzner',
|
|
||||||
};
|
|
||||||
Object.assign(newServer, { data });
|
|
||||||
await newServer.save();
|
|
||||||
return newServer;
|
|
||||||
}
|
|
||||||
|
|
||||||
// INSTANCE
|
|
||||||
@plugins.smartdata.unI()
|
|
||||||
public id: string;
|
|
||||||
|
|
||||||
@plugins.smartdata.svDb()
|
|
||||||
public data: plugins.servezoneInterfaces.data.IServer['data'];
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getServices(): Promise<plugins.servezoneInterfaces.data.IService[]> {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,110 +0,0 @@
|
|||||||
import * as plugins from '../plugins.js';
|
|
||||||
import { Cloudly } from '../classes.cloudly.js';
|
|
||||||
import { Cluster } from '../manager.cluster/classes.cluster.js';
|
|
||||||
import { Server } from './classes.server.js';
|
|
||||||
import { CurlFresh } from './classes.curlfresh.js';
|
|
||||||
|
|
||||||
export class CloudlyServerManager {
|
|
||||||
public cloudlyRef: Cloudly;
|
|
||||||
public typedRouter = new plugins.typedrequest.TypedRouter();
|
|
||||||
public curlfreshInstance = new CurlFresh(this);
|
|
||||||
|
|
||||||
public hetznerAccount: plugins.hetznercloud.HetznerAccount;
|
|
||||||
|
|
||||||
public get db() {
|
|
||||||
return this.cloudlyRef.mongodbConnector.smartdataDb;
|
|
||||||
}
|
|
||||||
public CServer = plugins.smartdata.setDefaultManagerForDoc(this, Server);
|
|
||||||
|
|
||||||
constructor(cloudlyRefArg: Cloudly) {
|
|
||||||
this.cloudlyRef = cloudlyRefArg;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* is used be serverconfig module on the server to get the actual server config
|
|
||||||
*/
|
|
||||||
this.typedRouter.addTypedHandler(
|
|
||||||
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.config.IRequest_Any_Cloudly_GetServerConfig>(
|
|
||||||
'getServerConfig',
|
|
||||||
async (requestData) => {
|
|
||||||
const serverId = requestData.serverId;
|
|
||||||
const server = await this.CServer.getInstance({
|
|
||||||
id: serverId,
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
configData: await server.createSavableObject(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async start() {
|
|
||||||
this.hetznerAccount = new plugins.hetznercloud.HetznerAccount(
|
|
||||||
this.cloudlyRef.config.data.hetznerToken,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async stop() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* creates the server infrastructure on hetzner
|
|
||||||
* ensures that there are exactly the reources that are needed
|
|
||||||
* no more, no less
|
|
||||||
*/
|
|
||||||
public async ensureServerInfrastructure() {
|
|
||||||
// get all clusters
|
|
||||||
const allClusters = await this.cloudlyRef.clusterManager.getAllClusters();
|
|
||||||
for (const cluster of allClusters) {
|
|
||||||
// get existing servers
|
|
||||||
const servers = await this.getServersByCluster(cluster);
|
|
||||||
|
|
||||||
// if there is no server, create one
|
|
||||||
if (servers.length === 0) {
|
|
||||||
const server = await this.hetznerAccount.createServer({
|
|
||||||
name: plugins.smartunique.uniSimple('server'),
|
|
||||||
location: 'nbg1',
|
|
||||||
type: 'cpx41',
|
|
||||||
labels: {
|
|
||||||
clusterId: cluster.id,
|
|
||||||
priority: '1',
|
|
||||||
},
|
|
||||||
userData: await this.curlfreshInstance.getServerUserData(),
|
|
||||||
});
|
|
||||||
const newServer = await Server.createFromHetznerServer(server);
|
|
||||||
console.log(`cluster created new server for cluster ${cluster.id}`);
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
`cluster ${cluster.id} already has servers. Making sure that they actually exist in the real world...`,
|
|
||||||
);
|
|
||||||
// if there is a server, make sure that it exists
|
|
||||||
for (const server of servers) {
|
|
||||||
const hetznerServer = await this.hetznerAccount.getServersByLabel({
|
|
||||||
clusterId: cluster.id,
|
|
||||||
});
|
|
||||||
if (!hetznerServer) {
|
|
||||||
console.log(`server ${server.id} does not exist in the real world. Creating it now...`);
|
|
||||||
const hetznerServer = await this.hetznerAccount.createServer({
|
|
||||||
name: plugins.smartunique.uniSimple('server'),
|
|
||||||
location: 'nbg1',
|
|
||||||
type: 'cpx41',
|
|
||||||
labels: {
|
|
||||||
clusterId: cluster.id,
|
|
||||||
priority: '1',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const newServer = await Server.createFromHetznerServer(hetznerServer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getServersByCluster(clusterArg: Cluster) {
|
|
||||||
const results = await this.CServer.getInstances({
|
|
||||||
data: {
|
|
||||||
assignedClusterId: clusterArg.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
}
|
|
255
ts/manager.settings/classes.settingsmanager.ts
Normal file
255
ts/manager.settings/classes.settingsmanager.ts
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import type { Cloudly } from '../classes.cloudly.js';
|
||||||
|
import * as servezoneInterfaces from '@serve.zone/interfaces';
|
||||||
|
|
||||||
|
export class CloudlySettingsManager {
|
||||||
|
public cloudlyRef: Cloudly;
|
||||||
|
public readyDeferred = plugins.smartpromise.defer();
|
||||||
|
public settingsStore: plugins.smartdata.EasyStore<servezoneInterfaces.data.ICloudlySettings>;
|
||||||
|
|
||||||
|
constructor(cloudlyRefArg: Cloudly) {
|
||||||
|
this.cloudlyRef = cloudlyRefArg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the settings manager and create the EasyStore
|
||||||
|
*/
|
||||||
|
public async init() {
|
||||||
|
this.settingsStore = await this.cloudlyRef.mongodbConnector.smartdataDb
|
||||||
|
.createEasyStore('cloudly-settings') as plugins.smartdata.EasyStore<servezoneInterfaces.data.ICloudlySettings>;
|
||||||
|
|
||||||
|
// Setup API route handlers
|
||||||
|
await this.setupRoutes();
|
||||||
|
|
||||||
|
this.readyDeferred.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all settings
|
||||||
|
*/
|
||||||
|
public async getSettings(): Promise<servezoneInterfaces.data.ICloudlySettings> {
|
||||||
|
await this.readyDeferred.promise;
|
||||||
|
return await this.settingsStore.readAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all settings with masked sensitive values (for API responses)
|
||||||
|
*/
|
||||||
|
public async getSettingsMasked(): Promise<servezoneInterfaces.data.ICloudlySettingsMasked> {
|
||||||
|
await this.readyDeferred.promise;
|
||||||
|
const settings = await this.getSettings();
|
||||||
|
const masked: servezoneInterfaces.data.ICloudlySettingsMasked = {};
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(settings)) {
|
||||||
|
if (typeof value === 'string' && value.length > 4) {
|
||||||
|
// Mask the token, showing only last 4 characters
|
||||||
|
masked[key] = '****' + value.slice(-4);
|
||||||
|
} else {
|
||||||
|
masked[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return masked;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update multiple settings at once
|
||||||
|
*/
|
||||||
|
public async updateSettings(updates: Partial<servezoneInterfaces.data.ICloudlySettings>): Promise<void> {
|
||||||
|
await this.readyDeferred.promise;
|
||||||
|
for (const [key, value] of Object.entries(updates)) {
|
||||||
|
if (value !== undefined && value !== '') {
|
||||||
|
await this.settingsStore.writeKey(key as keyof servezoneInterfaces.data.ICloudlySettings, value);
|
||||||
|
} else if (value === '') {
|
||||||
|
// Empty string means clear the setting
|
||||||
|
await this.settingsStore.deleteKey(key as keyof servezoneInterfaces.data.ICloudlySettings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific setting value
|
||||||
|
*/
|
||||||
|
public async getSetting<K extends keyof servezoneInterfaces.data.ICloudlySettings>(key: K): Promise<servezoneInterfaces.data.ICloudlySettings[K]> {
|
||||||
|
await this.readyDeferred.promise;
|
||||||
|
return await this.settingsStore.readKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a specific setting value
|
||||||
|
*/
|
||||||
|
public async setSetting<K extends keyof servezoneInterfaces.data.ICloudlySettings>(key: K, value: servezoneInterfaces.data.ICloudlySettings[K]): Promise<void> {
|
||||||
|
await this.readyDeferred.promise;
|
||||||
|
if (value !== undefined && value !== '') {
|
||||||
|
await this.settingsStore.writeKey(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear a specific setting
|
||||||
|
*/
|
||||||
|
public async clearSetting(key: keyof servezoneInterfaces.data.ICloudlySettings): Promise<void> {
|
||||||
|
await this.readyDeferred.promise;
|
||||||
|
await this.settingsStore.deleteKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all settings
|
||||||
|
*/
|
||||||
|
public async clearAllSettings(): Promise<void> {
|
||||||
|
await this.readyDeferred.promise;
|
||||||
|
await this.settingsStore.wipe();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test connection for a specific provider
|
||||||
|
*/
|
||||||
|
public async testProviderConnection(provider: string): Promise<{success: boolean; message: string}> {
|
||||||
|
await this.readyDeferred.promise;
|
||||||
|
try {
|
||||||
|
switch (provider) {
|
||||||
|
case 'hetzner':
|
||||||
|
const hetznerToken = await this.getSetting('hetznerToken');
|
||||||
|
if (!hetznerToken) {
|
||||||
|
return { success: false, message: 'No Hetzner token configured' };
|
||||||
|
}
|
||||||
|
// TODO: Implement actual Hetzner API test
|
||||||
|
return { success: true, message: 'Hetzner connection test successful' };
|
||||||
|
|
||||||
|
case 'cloudflare':
|
||||||
|
const cloudflareToken = await this.getSetting('cloudflareToken');
|
||||||
|
if (!cloudflareToken) {
|
||||||
|
return { success: false, message: 'No Cloudflare token configured' };
|
||||||
|
}
|
||||||
|
// TODO: Implement actual Cloudflare API test
|
||||||
|
return { success: true, message: 'Cloudflare connection test successful' };
|
||||||
|
|
||||||
|
case 'aws':
|
||||||
|
const awsKey = await this.getSetting('awsAccessKey');
|
||||||
|
const awsSecret = await this.getSetting('awsSecretKey');
|
||||||
|
if (!awsKey || !awsSecret) {
|
||||||
|
return { success: false, message: 'AWS credentials not configured' };
|
||||||
|
}
|
||||||
|
// TODO: Implement actual AWS API test
|
||||||
|
return { success: true, message: 'AWS connection test successful' };
|
||||||
|
|
||||||
|
case 'digitalocean':
|
||||||
|
const doToken = await this.getSetting('digitalOceanToken');
|
||||||
|
if (!doToken) {
|
||||||
|
return { success: false, message: 'No DigitalOcean token configured' };
|
||||||
|
}
|
||||||
|
// TODO: Implement actual DigitalOcean API test
|
||||||
|
return { success: true, message: 'DigitalOcean connection test successful' };
|
||||||
|
|
||||||
|
case 'azure':
|
||||||
|
const azureClientId = await this.getSetting('azureClientId');
|
||||||
|
const azureClientSecret = await this.getSetting('azureClientSecret');
|
||||||
|
const azureTenantId = await this.getSetting('azureTenantId');
|
||||||
|
if (!azureClientId || !azureClientSecret || !azureTenantId) {
|
||||||
|
return { success: false, message: 'Azure credentials not configured' };
|
||||||
|
}
|
||||||
|
// TODO: Implement actual Azure API test
|
||||||
|
return { success: true, message: 'Azure connection test successful' };
|
||||||
|
|
||||||
|
default:
|
||||||
|
return { success: false, message: `Unknown provider: ${provider}` };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, message: `Connection test failed: ${error.message}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup API route handlers for settings management
|
||||||
|
*/
|
||||||
|
private async setupRoutes() {
|
||||||
|
// Get Settings Handler
|
||||||
|
this.cloudlyRef.typedrouter.addTypedHandler<servezoneInterfaces.requests.settings.IRequest_GetSettings>(
|
||||||
|
new plugins.typedrequest.TypedHandler<servezoneInterfaces.requests.settings.IRequest_GetSettings>(
|
||||||
|
'getSettings',
|
||||||
|
async (requestData) => {
|
||||||
|
// TODO: Add authentication check for admin users
|
||||||
|
const maskedSettings = await this.getSettingsMasked();
|
||||||
|
return {
|
||||||
|
settings: maskedSettings
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update Settings Handler
|
||||||
|
this.cloudlyRef.typedrouter.addTypedHandler<servezoneInterfaces.requests.settings.IRequest_UpdateSettings>(
|
||||||
|
new plugins.typedrequest.TypedHandler<servezoneInterfaces.requests.settings.IRequest_UpdateSettings>(
|
||||||
|
'updateSettings',
|
||||||
|
async (requestData) => {
|
||||||
|
// TODO: Add authentication check for admin users
|
||||||
|
try {
|
||||||
|
await this.updateSettings(requestData.updates);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Settings updated successfully'
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Failed to update settings: ${error.message}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clear Setting Handler
|
||||||
|
this.cloudlyRef.typedrouter.addTypedHandler<servezoneInterfaces.requests.settings.IRequest_ClearSetting>(
|
||||||
|
new plugins.typedrequest.TypedHandler<servezoneInterfaces.requests.settings.IRequest_ClearSetting>(
|
||||||
|
'clearSetting',
|
||||||
|
async (requestData) => {
|
||||||
|
// TODO: Add authentication check for admin users
|
||||||
|
try {
|
||||||
|
await this.clearSetting(requestData.key);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Setting ${requestData.key} cleared successfully`
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Failed to clear setting: ${error.message}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test Provider Connection Handler
|
||||||
|
this.cloudlyRef.typedrouter.addTypedHandler<servezoneInterfaces.requests.settings.IRequest_TestProviderConnection>(
|
||||||
|
new plugins.typedrequest.TypedHandler<servezoneInterfaces.requests.settings.IRequest_TestProviderConnection>(
|
||||||
|
'testProviderConnection',
|
||||||
|
async (requestData) => {
|
||||||
|
// TODO: Add authentication check for admin users
|
||||||
|
const testResult = await this.testProviderConnection(requestData.provider);
|
||||||
|
return {
|
||||||
|
success: testResult.success,
|
||||||
|
message: testResult.message,
|
||||||
|
connectionValid: testResult.success
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get Single Setting Handler (for internal use)
|
||||||
|
this.cloudlyRef.typedrouter.addTypedHandler<servezoneInterfaces.requests.settings.IRequest_GetSetting>(
|
||||||
|
new plugins.typedrequest.TypedHandler<servezoneInterfaces.requests.settings.IRequest_GetSetting>(
|
||||||
|
'getSetting',
|
||||||
|
async (requestData) => {
|
||||||
|
// TODO: Add authentication check for admin users
|
||||||
|
const value = await this.getSetting(requestData.key);
|
||||||
|
return {
|
||||||
|
value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
1
ts/manager.settings/index.ts
Normal file
1
ts/manager.settings/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './classes.settingsmanager.js';
|
73
ts_interfaces/data/baremetal.ts
Normal file
73
ts_interfaces/data/baremetal.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
export interface IBareMetal {
|
||||||
|
id: string;
|
||||||
|
data: {
|
||||||
|
hostname: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IPMI management IP address
|
||||||
|
*/
|
||||||
|
ipmiAddress?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypted IPMI credentials
|
||||||
|
*/
|
||||||
|
ipmiCredentials?: {
|
||||||
|
username: string;
|
||||||
|
passwordEncrypted: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Primary network IP address
|
||||||
|
*/
|
||||||
|
primaryIp: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider of the physical server
|
||||||
|
*/
|
||||||
|
provider: 'hetzner' | 'aws' | 'digitalocean' | 'onpremise';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data center or location
|
||||||
|
*/
|
||||||
|
location: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hardware specifications
|
||||||
|
*/
|
||||||
|
specs: {
|
||||||
|
cpuModel: string;
|
||||||
|
cpuCores: number;
|
||||||
|
memoryGB: number;
|
||||||
|
storageGB: number;
|
||||||
|
storageType: 'ssd' | 'hdd' | 'nvme';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current power state
|
||||||
|
*/
|
||||||
|
powerState: 'on' | 'off' | 'unknown';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operating system information
|
||||||
|
*/
|
||||||
|
osInfo: {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
kernel?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of ClusterNode IDs running on this hardware
|
||||||
|
*/
|
||||||
|
assignedNodeIds: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata for provider-specific information
|
||||||
|
*/
|
||||||
|
providerMetadata?: {
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
@@ -1,8 +1,6 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
export interface ICloudlyConfig {
|
export interface ICloudlyConfig {
|
||||||
cfToken?: string;
|
|
||||||
hetznerToken?: string;
|
|
||||||
environment?: 'production' | 'integration';
|
environment?: 'production' | 'integration';
|
||||||
letsEncryptEmail?: string;
|
letsEncryptEmail?: string;
|
||||||
letsEncryptPrivateKey?: string;
|
letsEncryptPrivateKey?: string;
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
import { type IDockerRegistryInfo } from '../data/docker.js';
|
import { type IDockerRegistryInfo } from '../data/docker.js';
|
||||||
import type { IServer } from './server.js';
|
import type { IClusterNode } from './clusternode.js';
|
||||||
|
|
||||||
export interface ICluster {
|
export interface ICluster {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -19,9 +19,14 @@ export interface ICluster {
|
|||||||
cloudlyUrl?: string;
|
cloudlyUrl?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* what servers are expected to be part of the cluster
|
* Cluster setup mode - manual by default, or auto-provision with cloud provider
|
||||||
*/
|
*/
|
||||||
servers: IServer[];
|
setupMode?: 'manual' | 'hetzner' | 'aws' | 'digitalocean';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nodes that are part of the cluster
|
||||||
|
*/
|
||||||
|
nodes: IClusterNode[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ACME info. This is used to get SSL certificates.
|
* ACME info. This is used to get SSL certificates.
|
||||||
|
71
ts_interfaces/data/clusternode.ts
Normal file
71
ts_interfaces/data/clusternode.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
export interface IClusterNodeMetrics {
|
||||||
|
cpuUsagePercent: number;
|
||||||
|
memoryUsedMB: number;
|
||||||
|
memoryAvailableMB: number;
|
||||||
|
diskUsedGB: number;
|
||||||
|
diskAvailableGB: number;
|
||||||
|
containerCount: number;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IClusterNode {
|
||||||
|
id: string;
|
||||||
|
data: {
|
||||||
|
/**
|
||||||
|
* Reference to the cluster this node belongs to
|
||||||
|
*/
|
||||||
|
clusterId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to the physical server (if applicable)
|
||||||
|
*/
|
||||||
|
baremetalId?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of node
|
||||||
|
*/
|
||||||
|
nodeType: 'baremetal' | 'vm' | 'container';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current status of the node
|
||||||
|
*/
|
||||||
|
status: 'initializing' | 'online' | 'offline' | 'maintenance';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Role of the node in the cluster
|
||||||
|
*/
|
||||||
|
role: 'master' | 'worker';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp when node joined the cluster
|
||||||
|
*/
|
||||||
|
joinedAt: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last health check timestamp
|
||||||
|
*/
|
||||||
|
lastHealthCheck: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current metrics for the node
|
||||||
|
*/
|
||||||
|
metrics?: IClusterNodeMetrics;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Docker swarm node ID if part of swarm
|
||||||
|
*/
|
||||||
|
swarmNodeId?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSH keys deployed to this node
|
||||||
|
*/
|
||||||
|
sshKeys: plugins.tsclass.network.ISshKey[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debian packages installed on this node
|
||||||
|
*/
|
||||||
|
requiredDebianPackages: string[];
|
||||||
|
};
|
||||||
|
}
|
@@ -6,8 +6,58 @@ import * as plugins from '../plugins.js';
|
|||||||
*/
|
*/
|
||||||
export interface IDeployment {
|
export interface IDeployment {
|
||||||
id: string;
|
id: string;
|
||||||
affectedServiceIds: string[];
|
|
||||||
|
/**
|
||||||
|
* The service being deployed (single service per deployment)
|
||||||
|
*/
|
||||||
|
serviceId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The node this deployment is running on
|
||||||
|
*/
|
||||||
|
nodeId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Docker container ID for this deployment
|
||||||
|
*/
|
||||||
|
containerId?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Image used for this deployment
|
||||||
|
*/
|
||||||
usedImageId: string;
|
usedImageId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Version of the service deployed
|
||||||
|
*/
|
||||||
|
version: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp when deployed
|
||||||
|
*/
|
||||||
|
deployedAt: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deployment log entries
|
||||||
|
*/
|
||||||
deploymentLog: string[];
|
deploymentLog: string[];
|
||||||
status: 'scheduled' | 'running' | 'deployed' | 'failed';
|
|
||||||
|
/**
|
||||||
|
* Current status of the deployment
|
||||||
|
*/
|
||||||
|
status: 'scheduled' | 'starting' | 'running' | 'stopping' | 'stopped' | 'failed';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Health status of the deployment
|
||||||
|
*/
|
||||||
|
healthStatus?: 'healthy' | 'unhealthy' | 'unknown';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resource usage for this deployment
|
||||||
|
*/
|
||||||
|
resourceUsage?: {
|
||||||
|
cpuUsagePercent: number;
|
||||||
|
memoryUsedMB: number;
|
||||||
|
lastUpdated: number;
|
||||||
|
};
|
||||||
}
|
}
|
@@ -7,8 +7,10 @@ export * from './event.js';
|
|||||||
export * from './externalregistry.js';
|
export * from './externalregistry.js';
|
||||||
export * from './image.js';
|
export * from './image.js';
|
||||||
export * from './secretbundle.js';
|
export * from './secretbundle.js';
|
||||||
export * from './secretgroup.js'
|
export * from './secretgroup.js';
|
||||||
export * from './server.js';
|
export * from './baremetal.js';
|
||||||
|
export * from './clusternode.js';
|
||||||
|
export * from './settings.js';
|
||||||
export * from './service.js';
|
export * from './service.js';
|
||||||
export * from './status.js';
|
export * from './status.js';
|
||||||
export * from './traffic.js';
|
export * from './traffic.js';
|
||||||
|
@@ -17,6 +17,35 @@ export interface IService {
|
|||||||
* and thus live past the service lifecycle
|
* and thus live past the service lifecycle
|
||||||
*/
|
*/
|
||||||
additionalSecretBundleIds?: string[];
|
additionalSecretBundleIds?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service category determines deployment behavior
|
||||||
|
* - base: Core services that run on every node (coreflow, coretraffic, corelog)
|
||||||
|
* - distributed: Services that run on limited nodes (cores3, coremongo)
|
||||||
|
* - workload: User applications
|
||||||
|
*/
|
||||||
|
serviceCategory: 'base' | 'distributed' | 'workload';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deployment strategy for the service
|
||||||
|
* - all-nodes: Deploy to every node in the cluster
|
||||||
|
* - limited-replicas: Deploy to a limited number of nodes
|
||||||
|
* - custom: Custom deployment logic
|
||||||
|
*/
|
||||||
|
deploymentStrategy: 'all-nodes' | 'limited-replicas' | 'custom';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum number of replicas for distributed services
|
||||||
|
* For example, 3 for cores3 or coremongo
|
||||||
|
*/
|
||||||
|
maxReplicas?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to enforce anti-affinity rules
|
||||||
|
* When true, tries to spread deployments across different BareMetal servers
|
||||||
|
*/
|
||||||
|
antiAffinity?: boolean;
|
||||||
|
|
||||||
scaleFactor: number;
|
scaleFactor: number;
|
||||||
balancingStrategy: 'round-robin' | 'least-connections';
|
balancingStrategy: 'round-robin' | 'least-connections';
|
||||||
ports: {
|
ports: {
|
||||||
|
56
ts_interfaces/data/settings.ts
Normal file
56
ts_interfaces/data/settings.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for Cloudly settings stored in EasyStore
|
||||||
|
* These are runtime-configurable settings that can be modified via the UI
|
||||||
|
*/
|
||||||
|
export interface ICloudlySettings {
|
||||||
|
// Cloud Provider Tokens
|
||||||
|
hetznerToken?: string;
|
||||||
|
cloudflareToken?: string;
|
||||||
|
|
||||||
|
// AWS Credentials
|
||||||
|
awsAccessKey?: string;
|
||||||
|
awsSecretKey?: string;
|
||||||
|
awsRegion?: string;
|
||||||
|
|
||||||
|
// DigitalOcean
|
||||||
|
digitalOceanToken?: string;
|
||||||
|
|
||||||
|
// Azure Credentials
|
||||||
|
azureClientId?: string;
|
||||||
|
azureClientSecret?: string;
|
||||||
|
azureTenantId?: string;
|
||||||
|
azureSubscriptionId?: string;
|
||||||
|
|
||||||
|
// Google Cloud
|
||||||
|
googleCloudKeyJson?: string;
|
||||||
|
googleCloudProjectId?: string;
|
||||||
|
|
||||||
|
// Vultr
|
||||||
|
vultrApiKey?: string;
|
||||||
|
|
||||||
|
// Linode
|
||||||
|
linodeToken?: string;
|
||||||
|
|
||||||
|
// OVH
|
||||||
|
ovhApplicationKey?: string;
|
||||||
|
ovhApplicationSecret?: string;
|
||||||
|
ovhConsumerKey?: string;
|
||||||
|
|
||||||
|
// Scaleway
|
||||||
|
scalewayAccessKey?: string;
|
||||||
|
scalewaySecretKey?: string;
|
||||||
|
scalewayOrganizationId?: string;
|
||||||
|
|
||||||
|
// Other settings that might be added in the future
|
||||||
|
[key: string]: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for masked settings (used in API responses)
|
||||||
|
* Shows only last 4 characters of sensitive tokens
|
||||||
|
*/
|
||||||
|
export type ICloudlySettingsMasked = {
|
||||||
|
[K in keyof ICloudlySettings]: string | undefined;
|
||||||
|
};
|
22
ts_interfaces/requests/baremetal.ts
Normal file
22
ts_interfaces/requests/baremetal.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import type { IBareMetal } from '../data/baremetal.js';
|
||||||
|
|
||||||
|
export interface IRequest_Any_Cloudly_GetBaremetalServers {
|
||||||
|
method: 'getBaremetalServers';
|
||||||
|
request: {};
|
||||||
|
response: {
|
||||||
|
baremetals: IBareMetal[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRequest_Any_Cloudly_ControlBaremetal {
|
||||||
|
method: 'controlBaremetal';
|
||||||
|
request: {
|
||||||
|
baremetalId: string;
|
||||||
|
action: 'powerOn' | 'powerOff' | 'reset';
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
}
|
@@ -41,6 +41,7 @@ export interface IRequest_CreateCluster extends plugins.typedrequestInterfaces.i
|
|||||||
request: {
|
request: {
|
||||||
identity: userInterfaces.IIdentity;
|
identity: userInterfaces.IIdentity;
|
||||||
clusterName: string;
|
clusterName: string;
|
||||||
|
setupMode?: 'manual' | 'hetzner' | 'aws' | 'digitalocean';
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
cluster: clusterInterfaces.ICluster;
|
cluster: clusterInterfaces.ICluster;
|
||||||
|
141
ts_interfaces/requests/deployment.ts
Normal file
141
ts_interfaces/requests/deployment.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import type { IDeployment } from '../data/deployment.js';
|
||||||
|
import type { IIdentity } from '../data/user.js';
|
||||||
|
|
||||||
|
export interface IReq_Any_Cloudly_GetDeploymentById
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_Any_Cloudly_GetDeploymentById
|
||||||
|
> {
|
||||||
|
method: 'getDeploymentById';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
deploymentId: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
deployment: IDeployment;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_Any_Cloudly_GetDeployments
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_Any_Cloudly_GetDeployments
|
||||||
|
> {
|
||||||
|
method: 'getDeployments';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
deployments: IDeployment[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_Any_Cloudly_GetDeploymentsByService
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_Any_Cloudly_GetDeploymentsByService
|
||||||
|
> {
|
||||||
|
method: 'getDeploymentsByService';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
serviceId: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
deployments: IDeployment[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_Any_Cloudly_GetDeploymentsByNode
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_Any_Cloudly_GetDeploymentsByNode
|
||||||
|
> {
|
||||||
|
method: 'getDeploymentsByNode';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
nodeId: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
deployments: IDeployment[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_Any_Cloudly_CreateDeployment
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_Any_Cloudly_CreateDeployment
|
||||||
|
> {
|
||||||
|
method: 'createDeployment';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
deploymentData: Partial<IDeployment>;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
deployment: IDeployment;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_Any_Cloudly_UpdateDeployment
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_Any_Cloudly_UpdateDeployment
|
||||||
|
> {
|
||||||
|
method: 'updateDeployment';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
deploymentId: string;
|
||||||
|
deploymentData: Partial<IDeployment>;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
deployment: IDeployment;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_Any_Cloudly_DeleteDeploymentById
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_Any_Cloudly_DeleteDeploymentById
|
||||||
|
> {
|
||||||
|
method: 'deleteDeploymentById';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
deploymentId: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_Any_Cloudly_RestartDeployment
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_Any_Cloudly_RestartDeployment
|
||||||
|
> {
|
||||||
|
method: 'restartDeployment';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
deploymentId: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
deployment: IDeployment;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_Any_Cloudly_ScaleDeployment
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_Any_Cloudly_ScaleDeployment
|
||||||
|
> {
|
||||||
|
method: 'scaleDeployment';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
deploymentId: string;
|
||||||
|
replicas: number;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
deployment: IDeployment;
|
||||||
|
};
|
||||||
|
}
|
@@ -1,6 +1,7 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
|
|
||||||
import * as adminRequests from './admin.js';
|
import * as adminRequests from './admin.js';
|
||||||
|
import * as baremetalRequests from './baremetal.js';
|
||||||
import * as certificateRequests from './certificate.js';
|
import * as certificateRequests from './certificate.js';
|
||||||
import * as clusterRequests from './cluster.js';
|
import * as clusterRequests from './cluster.js';
|
||||||
import * as configRequests from './config.js';
|
import * as configRequests from './config.js';
|
||||||
@@ -10,16 +11,19 @@ import * as imageRequests from './image.js';
|
|||||||
import * as informRequests from './inform.js';
|
import * as informRequests from './inform.js';
|
||||||
import * as logRequests from './log.js';
|
import * as logRequests from './log.js';
|
||||||
import * as networkRequests from './network.js';
|
import * as networkRequests from './network.js';
|
||||||
|
import * as nodeRequests from './node.js';
|
||||||
import * as routingRequests from './routing.js';
|
import * as routingRequests from './routing.js';
|
||||||
import * as secretBundleRequests from './secretbundle.js';
|
import * as secretBundleRequests from './secretbundle.js';
|
||||||
import * as secretGroupRequests from './secretgroup.js';
|
import * as secretGroupRequests from './secretgroup.js';
|
||||||
import * as serverRequests from './server.js';
|
import * as serverRequests from './server.js';
|
||||||
import * as serviceRequests from './service.js';
|
import * as serviceRequests from './service.js';
|
||||||
|
import * as settingsRequests from './settings.js';
|
||||||
import * as statusRequests from './status.js';
|
import * as statusRequests from './status.js';
|
||||||
import * as versionRequests from './version.js';
|
import * as versionRequests from './version.js';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
adminRequests as admin,
|
adminRequests as admin,
|
||||||
|
baremetalRequests as baremetal,
|
||||||
certificateRequests as certificate,
|
certificateRequests as certificate,
|
||||||
clusterRequests as cluster,
|
clusterRequests as cluster,
|
||||||
configRequests as config,
|
configRequests as config,
|
||||||
@@ -29,11 +33,13 @@ export {
|
|||||||
informRequests as inform,
|
informRequests as inform,
|
||||||
logRequests as log,
|
logRequests as log,
|
||||||
networkRequests as network,
|
networkRequests as network,
|
||||||
|
nodeRequests as node,
|
||||||
routingRequests as routing,
|
routingRequests as routing,
|
||||||
secretBundleRequests as secretbundle,
|
secretBundleRequests as secretbundle,
|
||||||
secretGroupRequests as secretgroup,
|
secretGroupRequests as secretgroup,
|
||||||
serverRequests as server,
|
serverRequests as server,
|
||||||
serviceRequests as service,
|
serviceRequests as service,
|
||||||
|
settingsRequests as settings,
|
||||||
statusRequests as status,
|
statusRequests as status,
|
||||||
versionRequests as version,
|
versionRequests as version,
|
||||||
};
|
};
|
||||||
|
33
ts_interfaces/requests/node.ts
Normal file
33
ts_interfaces/requests/node.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import type { IClusterNode } from '../data/clusternode.js';
|
||||||
|
import type { IDeployment } from '../data/deployment.js';
|
||||||
|
|
||||||
|
export interface IRequest_Any_Cloudly_GetNodeConfig {
|
||||||
|
method: 'getNodeConfig';
|
||||||
|
request: {
|
||||||
|
nodeId: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
configData: IClusterNode;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRequest_Any_Cloudly_GetNodesByCluster {
|
||||||
|
method: 'getNodesByCluster';
|
||||||
|
request: {
|
||||||
|
clusterId: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
nodes: IClusterNode[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRequest_Any_Cloudly_GetNodeDeployments {
|
||||||
|
method: 'getNodeDeployments';
|
||||||
|
request: {
|
||||||
|
nodeId: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
deployments: IDeployment[];
|
||||||
|
};
|
||||||
|
}
|
59
ts_interfaces/requests/settings.ts
Normal file
59
ts_interfaces/requests/settings.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import type { ICloudlySettings, ICloudlySettingsMasked } from '../data/settings.js';
|
||||||
|
|
||||||
|
// Get Settings
|
||||||
|
export interface IRequest_GetSettings extends plugins.typedrequestInterfaces.ITypedRequest {
|
||||||
|
method: 'getSettings';
|
||||||
|
request: {};
|
||||||
|
response: {
|
||||||
|
settings: ICloudlySettingsMasked;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Settings
|
||||||
|
export interface IRequest_UpdateSettings extends plugins.typedrequestInterfaces.ITypedRequest {
|
||||||
|
method: 'updateSettings';
|
||||||
|
request: {
|
||||||
|
updates: Partial<ICloudlySettings>;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear Specific Setting
|
||||||
|
export interface IRequest_ClearSetting extends plugins.typedrequestInterfaces.ITypedRequest {
|
||||||
|
method: 'clearSetting';
|
||||||
|
request: {
|
||||||
|
key: keyof ICloudlySettings;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Provider Connection
|
||||||
|
export interface IRequest_TestProviderConnection extends plugins.typedrequestInterfaces.ITypedRequest {
|
||||||
|
method: 'testProviderConnection';
|
||||||
|
request: {
|
||||||
|
provider: 'hetzner' | 'cloudflare' | 'aws' | 'digitalocean' | 'azure' | 'google' | 'vultr' | 'linode' | 'ovh' | 'scaleway';
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
connectionValid: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Single Setting (for internal use, not exposed to frontend)
|
||||||
|
export interface IRequest_GetSetting extends plugins.typedrequestInterfaces.ITypedRequest {
|
||||||
|
method: 'getSetting';
|
||||||
|
request: {
|
||||||
|
key: keyof ICloudlySettings;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
value: string | undefined;
|
||||||
|
};
|
||||||
|
}
|
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/cloudly',
|
name: '@serve.zone/cloudly',
|
||||||
version: '5.0.5',
|
version: '5.3.0',
|
||||||
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.'
|
||||||
}
|
}
|
||||||
|
@@ -48,8 +48,8 @@ export interface IDataState {
|
|||||||
secretBundles?: plugins.interfaces.data.ISecretBundle[];
|
secretBundles?: plugins.interfaces.data.ISecretBundle[];
|
||||||
clusters?: plugins.interfaces.data.ICluster[];
|
clusters?: plugins.interfaces.data.ICluster[];
|
||||||
images?: any[];
|
images?: any[];
|
||||||
services?: any[];
|
services?: plugins.interfaces.data.IService[];
|
||||||
deployments?: any[];
|
deployments?: plugins.interfaces.data.IDeployment[];
|
||||||
dns?: any[];
|
dns?: any[];
|
||||||
mails?: any[];
|
mails?: any[];
|
||||||
logs?: any[];
|
logs?: any[];
|
||||||
@@ -136,9 +136,90 @@ export const getAllDataAction = dataState.createAction(async (statePartArg) => {
|
|||||||
clusters: responseClusters.clusters,
|
clusters: responseClusters.clusters,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Services
|
||||||
|
const trGetServices =
|
||||||
|
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.service.IRequest_Any_Cloudly_GetServices>(
|
||||||
|
'/typedrequest',
|
||||||
|
'getServices'
|
||||||
|
);
|
||||||
|
const responseServices = await trGetServices.fire({
|
||||||
|
identity: loginStatePart.getState().identity,
|
||||||
|
});
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
services: responseServices.services,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Deployments
|
||||||
|
const trGetDeployments =
|
||||||
|
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.deployment.IReq_Any_Cloudly_GetDeployments>(
|
||||||
|
'/typedrequest',
|
||||||
|
'getDeployments'
|
||||||
|
);
|
||||||
|
const responseDeployments = await trGetDeployments.fire({
|
||||||
|
identity: loginStatePart.getState().identity,
|
||||||
|
});
|
||||||
|
currentState = {
|
||||||
|
...currentState,
|
||||||
|
deployments: responseDeployments.deployments,
|
||||||
|
};
|
||||||
|
|
||||||
return currentState;
|
return currentState;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Service Actions
|
||||||
|
export const createServiceAction = dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { serviceData: plugins.interfaces.data.IService['data'] }) => {
|
||||||
|
let currentState = statePartArg.getState();
|
||||||
|
const trCreateService =
|
||||||
|
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.service.IRequest_Any_Cloudly_CreateService>(
|
||||||
|
'/typedrequest',
|
||||||
|
'createService'
|
||||||
|
);
|
||||||
|
const response = await trCreateService.fire({
|
||||||
|
identity: loginStatePart.getState().identity,
|
||||||
|
serviceData: payloadArg.serviceData,
|
||||||
|
});
|
||||||
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const updateServiceAction = dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { serviceId: string; serviceData: plugins.interfaces.data.IService['data'] }) => {
|
||||||
|
let currentState = statePartArg.getState();
|
||||||
|
const trUpdateService =
|
||||||
|
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.service.IRequest_Any_Cloudly_UpdateService>(
|
||||||
|
'/typedrequest',
|
||||||
|
'updateService'
|
||||||
|
);
|
||||||
|
const response = await trUpdateService.fire({
|
||||||
|
identity: loginStatePart.getState().identity,
|
||||||
|
serviceId: payloadArg.serviceId,
|
||||||
|
serviceData: payloadArg.serviceData,
|
||||||
|
});
|
||||||
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const deleteServiceAction = dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { serviceId: string }) => {
|
||||||
|
let currentState = statePartArg.getState();
|
||||||
|
const trDeleteService =
|
||||||
|
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.service.IRequest_Any_Cloudly_DeleteServiceById>(
|
||||||
|
'/typedrequest',
|
||||||
|
'deleteServiceById'
|
||||||
|
);
|
||||||
|
const response = await trDeleteService.fire({
|
||||||
|
identity: loginStatePart.getState().identity,
|
||||||
|
serviceId: payloadArg.serviceId,
|
||||||
|
});
|
||||||
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
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: plugins.interfaces.data.ISecretGroup) => {
|
||||||
@@ -239,12 +320,66 @@ export const deleteImageAction = dataState.createAction(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Deployment Actions
|
||||||
|
export const createDeploymentAction = dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { deploymentData: Partial<plugins.interfaces.data.IDeployment> }) => {
|
||||||
|
let currentState = statePartArg.getState();
|
||||||
|
const trCreateDeployment =
|
||||||
|
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.deployment.IReq_Any_Cloudly_CreateDeployment>(
|
||||||
|
'/typedrequest',
|
||||||
|
'createDeployment'
|
||||||
|
);
|
||||||
|
const response = await trCreateDeployment.fire({
|
||||||
|
identity: loginStatePart.getState().identity,
|
||||||
|
deploymentData: payloadArg.deploymentData,
|
||||||
|
});
|
||||||
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const updateDeploymentAction = dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { deploymentId: string; deploymentData: Partial<plugins.interfaces.data.IDeployment> }) => {
|
||||||
|
let currentState = statePartArg.getState();
|
||||||
|
const trUpdateDeployment =
|
||||||
|
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.deployment.IReq_Any_Cloudly_UpdateDeployment>(
|
||||||
|
'/typedrequest',
|
||||||
|
'updateDeployment'
|
||||||
|
);
|
||||||
|
const response = await trUpdateDeployment.fire({
|
||||||
|
identity: loginStatePart.getState().identity,
|
||||||
|
deploymentId: payloadArg.deploymentId,
|
||||||
|
deploymentData: payloadArg.deploymentData,
|
||||||
|
});
|
||||||
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const deleteDeploymentAction = dataState.createAction(
|
||||||
|
async (statePartArg, payloadArg: { deploymentId: string }) => {
|
||||||
|
let currentState = statePartArg.getState();
|
||||||
|
const trDeleteDeployment =
|
||||||
|
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.deployment.IReq_Any_Cloudly_DeleteDeploymentById>(
|
||||||
|
'/typedrequest',
|
||||||
|
'deleteDeploymentById'
|
||||||
|
);
|
||||||
|
const response = await trDeleteDeployment.fire({
|
||||||
|
identity: loginStatePart.getState().identity,
|
||||||
|
deploymentId: payloadArg.deploymentId,
|
||||||
|
});
|
||||||
|
currentState = await dataState.dispatchAction(getAllDataAction, null);
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// cluster
|
// cluster
|
||||||
export const addClusterAction = dataState.createAction(
|
export const addClusterAction = dataState.createAction(
|
||||||
async (
|
async (
|
||||||
statePartArg,
|
statePartArg,
|
||||||
payloadArg: {
|
payloadArg: {
|
||||||
clusterName: string;
|
clusterName: string;
|
||||||
|
setupMode?: 'manual' | 'hetzner' | 'aws' | 'digitalocean';
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState();
|
||||||
|
@@ -25,6 +25,7 @@ import { CloudlyViewSecretBundles } from './cloudly-view-secretbundles.js';
|
|||||||
import { CloudlyViewSecretGroups } from './cloudly-view-secretgroups.js';
|
import { CloudlyViewSecretGroups } from './cloudly-view-secretgroups.js';
|
||||||
import { CloudlyViewServices } from './cloudly-view-services.js';
|
import { CloudlyViewServices } from './cloudly-view-services.js';
|
||||||
import { CloudlyViewExternalRegistries } from './cloudly-view-externalregistries.js';
|
import { CloudlyViewExternalRegistries } from './cloudly-view-externalregistries.js';
|
||||||
|
import { CloudlyViewSettings } from './cloudly-view-settings.js';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -76,66 +77,87 @@ export class CloudlyDashboard extends DeesElement {
|
|||||||
.viewTabs=${[
|
.viewTabs=${[
|
||||||
{
|
{
|
||||||
name: 'Overview',
|
name: 'Overview',
|
||||||
|
iconName: 'lucide:LayoutDashboard',
|
||||||
element: CloudlyViewOverview,
|
element: CloudlyViewOverview,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Settings',
|
||||||
|
iconName: 'lucide:Settings',
|
||||||
|
element: CloudlyViewSettings,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'SecretGroups',
|
name: 'SecretGroups',
|
||||||
|
iconName: 'lucide:ShieldCheck',
|
||||||
element: CloudlyViewSecretGroups,
|
element: CloudlyViewSecretGroups,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'SecretBundles',
|
name: 'SecretBundles',
|
||||||
|
iconName: 'lucide:LockKeyhole',
|
||||||
element: CloudlyViewSecretBundles,
|
element: CloudlyViewSecretBundles,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Clusters',
|
name: 'Clusters',
|
||||||
|
iconName: 'lucide:Network',
|
||||||
element: CloudlyViewClusters,
|
element: CloudlyViewClusters,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ExternalRegistries',
|
name: 'ExternalRegistries',
|
||||||
|
iconName: 'lucide:Package',
|
||||||
element: CloudlyViewExternalRegistries,
|
element: CloudlyViewExternalRegistries,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Images',
|
name: 'Images',
|
||||||
|
iconName: 'lucide:Image',
|
||||||
element: CloudlyViewImages,
|
element: CloudlyViewImages,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Services',
|
name: 'Services',
|
||||||
|
iconName: 'lucide:Layers',
|
||||||
element: CloudlyViewServices,
|
element: CloudlyViewServices,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Testing & Building',
|
name: 'Testing & Building',
|
||||||
|
iconName: 'lucide:HardHat',
|
||||||
element: CloudlyViewServices,
|
element: CloudlyViewServices,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Deployments',
|
name: 'Deployments',
|
||||||
|
iconName: 'lucide:Rocket',
|
||||||
element: CloudlyViewDeployments,
|
element: CloudlyViewDeployments,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'DNS',
|
name: 'DNS',
|
||||||
|
iconName: 'lucide:Globe',
|
||||||
element: CloudlyViewDns,
|
element: CloudlyViewDns,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Mails',
|
name: 'Mails',
|
||||||
|
iconName: 'lucide:Mail',
|
||||||
element: CloudlyViewMails,
|
element: CloudlyViewMails,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Logs',
|
name: 'Logs',
|
||||||
|
iconName: 'lucide:FileText',
|
||||||
element: CloudlyViewLogs,
|
element: CloudlyViewLogs,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 's3',
|
name: 's3',
|
||||||
|
iconName: 'lucide:Cloud',
|
||||||
element: CloudlyViewS3,
|
element: CloudlyViewS3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'DBs',
|
name: 'DBs',
|
||||||
|
iconName: 'lucide:Database',
|
||||||
element: CloudlyViewDbs,
|
element: CloudlyViewDbs,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Backups',
|
name: 'Backups',
|
||||||
|
iconName: 'lucide:Save',
|
||||||
element: CloudlyViewBackups,
|
element: CloudlyViewBackups,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Fleet',
|
name: 'Fleet',
|
||||||
|
iconName: 'lucide:Truck',
|
||||||
element: CloudlyViewBackups,
|
element: CloudlyViewBackups,
|
||||||
}
|
}
|
||||||
] as plugins.deesCatalog.IView[]}
|
] as plugins.deesCatalog.IView[]}
|
||||||
|
@@ -68,6 +68,18 @@ export class CloudlyViewClusters extends DeesElement {
|
|||||||
.description=${'a descriptive name for the cluster'}
|
.description=${'a descriptive name for the cluster'}
|
||||||
.value=${''}
|
.value=${''}
|
||||||
></dees-input-text>
|
></dees-input-text>
|
||||||
|
<dees-input-dropdown
|
||||||
|
.key=${'setupMode'}
|
||||||
|
.label=${'Setup Mode'}
|
||||||
|
.description=${'How the cluster infrastructure should be managed'}
|
||||||
|
.options=${[
|
||||||
|
{option: 'manual', key: 'manual', description: 'Manual Setup - Add your own servers manually'},
|
||||||
|
{option: 'hetzner', key: 'hetzner', description: 'Hetzner Cloud - Auto-provision servers on Hetzner'},
|
||||||
|
{option: 'aws', key: 'aws', description: 'AWS - Auto-provision on Amazon Web Services (coming soon)', disabled: true},
|
||||||
|
{option: 'digitalocean', key: 'digitalocean', description: 'DigitalOcean - Auto-provision on DigitalOcean (coming soon)', disabled: true}
|
||||||
|
]}
|
||||||
|
.selectedOption=${'manual'}
|
||||||
|
></dees-input-dropdown>
|
||||||
</dees-form>
|
</dees-form>
|
||||||
`,
|
`,
|
||||||
menuOptions: [
|
menuOptions: [
|
||||||
@@ -76,6 +88,7 @@ export class CloudlyViewClusters extends DeesElement {
|
|||||||
action: async (modalArg) => {
|
action: async (modalArg) => {
|
||||||
const data: {
|
const data: {
|
||||||
clusterName: string;
|
clusterName: string;
|
||||||
|
setupMode: 'manual' | 'hetzner' | 'aws' | 'digitalocean';
|
||||||
} = (await modalArg.shadowRoot
|
} = (await modalArg.shadowRoot
|
||||||
.querySelector('dees-form')
|
.querySelector('dees-form')
|
||||||
.collectFormData()) as any;
|
.collectFormData()) as any;
|
||||||
|
@@ -22,62 +22,222 @@ export class CloudlyViewDeployments extends DeesElement {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
const subecription = appstate.dataState
|
const subscription = appstate.dataState
|
||||||
.select((stateArg) => stateArg)
|
.select((stateArg) => stateArg)
|
||||||
.subscribe((dataArg) => {
|
.subscribe((dataArg) => {
|
||||||
this.data = dataArg;
|
this.data = dataArg;
|
||||||
});
|
});
|
||||||
this.rxSubscriptions.push(subecription);
|
this.rxSubscriptions.push(subscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
shared.viewHostCss,
|
shared.viewHostCss,
|
||||||
css`
|
css`
|
||||||
|
.status-badge {
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.status-running {
|
||||||
|
background: #4caf50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.status-stopped {
|
||||||
|
background: #f44336;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.status-paused {
|
||||||
|
background: #ff9800;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.status-deploying {
|
||||||
|
background: #2196f3;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.health-indicator {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
.health-healthy {
|
||||||
|
background: #e8f5e9;
|
||||||
|
color: #2e7d32;
|
||||||
|
}
|
||||||
|
.health-unhealthy {
|
||||||
|
background: #ffebee;
|
||||||
|
color: #c62828;
|
||||||
|
}
|
||||||
|
.health-unknown {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.resource-usage {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
.resource-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private getServiceName(serviceId: string): string {
|
||||||
|
const service = this.data.services?.find(s => s.id === serviceId);
|
||||||
|
return service?.data?.name || serviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getNodeName(nodeId: string): string {
|
||||||
|
// This would ideally look up the cluster node name
|
||||||
|
// For now just return the ID shortened
|
||||||
|
return nodeId.substring(0, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getStatusBadgeHtml(status: string): any {
|
||||||
|
const className = `status-badge status-${status}`;
|
||||||
|
return html`<span class="${className}">${status}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getHealthIndicatorHtml(health?: string): any {
|
||||||
|
if (!health) health = 'unknown';
|
||||||
|
const className = `health-indicator health-${health}`;
|
||||||
|
const icon = health === 'healthy' ? '✓' : health === 'unhealthy' ? '✗' : '?';
|
||||||
|
return html`<span class="${className}">${icon} ${health}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getResourceUsageHtml(deployment: plugins.interfaces.data.IDeployment): any {
|
||||||
|
if (!deployment.resourceUsage) {
|
||||||
|
return html`<span style="color: #aaa;">N/A</span>`;
|
||||||
|
}
|
||||||
|
const { cpuUsagePercent, memoryUsedMB } = deployment.resourceUsage;
|
||||||
|
return html`
|
||||||
|
<div class="resource-usage">
|
||||||
|
<div class="resource-item">
|
||||||
|
<lucide-icon name="Cpu" size="14"></lucide-icon>
|
||||||
|
${cpuUsagePercent?.toFixed(1) || 0}%
|
||||||
|
</div>
|
||||||
|
<div class="resource-item">
|
||||||
|
<lucide-icon name="MemoryStick" size="14"></lucide-icon>
|
||||||
|
${memoryUsedMB || 0} MB
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
return html`
|
return html`
|
||||||
<cloudly-sectionheading>Deployments</cloudly-sectionheading>
|
<cloudly-sectionheading>Deployments</cloudly-sectionheading>
|
||||||
<dees-table
|
<dees-table
|
||||||
.heading1=${'Deployments'}
|
.heading1=${'Deployments'}
|
||||||
.heading2=${'decoded in client'}
|
.heading2=${'Service deployments running on cluster nodes'}
|
||||||
.data=${this.data.deployments}
|
.data=${this.data.deployments || []}
|
||||||
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
|
.displayFunction=${(itemArg: plugins.interfaces.data.IDeployment) => {
|
||||||
return {
|
return {
|
||||||
id: itemArg.id,
|
Service: this.getServiceName(itemArg.serviceId),
|
||||||
serverAmount: itemArg.data.servers.length,
|
Node: this.getNodeName(itemArg.nodeId),
|
||||||
|
Status: this.getStatusBadgeHtml(itemArg.status),
|
||||||
|
Health: this.getHealthIndicatorHtml(itemArg.healthStatus),
|
||||||
|
'Container ID': itemArg.containerId ?
|
||||||
|
html`<span style="font-family: monospace; font-size: 0.9em;">${itemArg.containerId.substring(0, 12)}</span>` :
|
||||||
|
html`<span style="color: #aaa;">N/A</span>`,
|
||||||
|
Version: itemArg.version || 'latest',
|
||||||
|
'Resource Usage': this.getResourceUsageHtml(itemArg),
|
||||||
|
'Last Updated': itemArg.deployedAt ?
|
||||||
|
new Date(itemArg.deployedAt).toLocaleString() :
|
||||||
|
'Never',
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
.dataActions=${[
|
.dataActions=${[
|
||||||
{
|
{
|
||||||
name: 'add configBundle',
|
name: 'Deploy Service',
|
||||||
iconName: 'plus',
|
iconName: 'plus',
|
||||||
type: ['header', 'footer'],
|
type: ['header', 'footer'],
|
||||||
actionFunc: async (dataActionArg) => {
|
actionFunc: async (dataActionArg) => {
|
||||||
|
const availableServices = this.data.services || [];
|
||||||
|
if (availableServices.length === 0) {
|
||||||
|
plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
|
heading: 'No Services Available',
|
||||||
|
content: html`
|
||||||
|
<div style="text-align: center; padding: 24px;">
|
||||||
|
<lucide-icon name="AlertCircle" size="48" style="color: #ff9800; margin-bottom: 16px;"></lucide-icon>
|
||||||
|
<div>Please create a service first before creating deployments.</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: 'OK',
|
||||||
|
action: async (modalArg) => {
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
heading: 'Add ConfigBundle',
|
heading: 'Deploy Service',
|
||||||
content: html`
|
content: html`
|
||||||
<dees-form>
|
<dees-form>
|
||||||
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
<dees-input-dropdown
|
||||||
<dees-input-text
|
.key=${'serviceId'}
|
||||||
.key=${'data.secretGroupIds'}
|
.label=${'Service'}
|
||||||
.label=${'secretGroupIds'}
|
.options=${availableServices.map(s => ({ key: s.id, value: s.data.name }))}
|
||||||
.value=${''}
|
.required=${true}>
|
||||||
></dees-input-text>
|
</dees-input-dropdown>
|
||||||
<dees-input-text
|
<dees-input-text
|
||||||
.key=${'data.includedTags'}
|
.key=${'nodeId'}
|
||||||
.label=${'includedTags'}
|
.label=${'Target Node ID'}
|
||||||
.value=${''}
|
.required=${true}
|
||||||
></dees-input-text>
|
.description=${'Enter the cluster node ID where this service should be deployed'}>
|
||||||
|
</dees-input-text>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'version'}
|
||||||
|
.label=${'Version'}
|
||||||
|
.value=${'latest'}
|
||||||
|
.required=${true}>
|
||||||
|
</dees-input-text>
|
||||||
|
<dees-input-dropdown
|
||||||
|
.key=${'status'}
|
||||||
|
.label=${'Initial Status'}
|
||||||
|
.options=${['deploying', 'running']}
|
||||||
|
.value=${'deploying'}
|
||||||
|
.required=${true}>
|
||||||
|
</dees-input-dropdown>
|
||||||
</dees-form>
|
</dees-form>
|
||||||
`,
|
`,
|
||||||
menuOptions: [
|
menuOptions: [
|
||||||
{ name: 'create', action: async (modalArg) => {} },
|
|
||||||
{
|
{
|
||||||
name: 'cancel',
|
name: 'Deploy',
|
||||||
|
action: async (modalArg) => {
|
||||||
|
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
|
||||||
|
const formData = await form.gatherData();
|
||||||
|
|
||||||
|
await appstate.dataState.dispatchAction(appstate.createDeploymentAction, {
|
||||||
|
deploymentData: {
|
||||||
|
serviceId: formData.serviceId,
|
||||||
|
nodeId: formData.nodeId,
|
||||||
|
status: formData.status,
|
||||||
|
version: formData.version,
|
||||||
|
deployedAt: Date.now(),
|
||||||
|
usedImageId: 'placeholder', // This would come from the service
|
||||||
|
deploymentLog: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Cancel',
|
||||||
action: async (modalArg) => {
|
action: async (modalArg) => {
|
||||||
modalArg.destroy();
|
modalArg.destroy();
|
||||||
},
|
},
|
||||||
@@ -87,34 +247,96 @@ export class CloudlyViewDeployments extends DeesElement {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'delete',
|
name: 'Restart',
|
||||||
iconName: 'trash',
|
iconName: 'refresh-cw',
|
||||||
type: ['contextmenu', 'inRow'],
|
type: ['contextmenu', 'inRow'],
|
||||||
actionFunc: async (actionDataArg) => {
|
actionFunc: async (actionDataArg) => {
|
||||||
|
const deployment = actionDataArg.item as plugins.interfaces.data.IDeployment;
|
||||||
plugins.deesCatalog.DeesModal.createAndShow({
|
plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
|
heading: `Restart Deployment`,
|
||||||
content: html`
|
content: html`
|
||||||
<div style="text-align:center">
|
<div style="text-align:center">
|
||||||
Do you really want to delete the ConfigBundle?
|
Are you sure you want to restart this deployment?
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;">
|
||||||
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
|
<div style="color: #fff; font-weight: bold;">
|
||||||
>
|
${this.getServiceName(deployment.serviceId)}
|
||||||
${actionDataArg.item.id}
|
</div>
|
||||||
|
<div style="color: #aaa; font-size: 0.9em; margin-top: 4px;">
|
||||||
|
Node: ${this.getNodeName(deployment.nodeId)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
menuOptions: [
|
menuOptions: [
|
||||||
{
|
{
|
||||||
name: 'cancel',
|
name: 'Cancel',
|
||||||
action: async (modalArg) => {
|
action: async (modalArg) => {
|
||||||
await modalArg.destroy();
|
await modalArg.destroy();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'delete',
|
name: 'Restart',
|
||||||
action: async (modalArg) => {
|
action: async (modalArg) => {
|
||||||
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
|
// TODO: Implement restart action
|
||||||
configBundleId: actionDataArg.item.id,
|
console.log('Restart deployment:', deployment);
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Stop',
|
||||||
|
iconName: 'square',
|
||||||
|
type: ['contextmenu', 'inRow'],
|
||||||
|
actionFunc: async (actionDataArg) => {
|
||||||
|
const deployment = actionDataArg.item as plugins.interfaces.data.IDeployment;
|
||||||
|
await appstate.dataState.dispatchAction(appstate.updateDeploymentAction, {
|
||||||
|
deploymentId: deployment.id,
|
||||||
|
deploymentData: {
|
||||||
|
...deployment,
|
||||||
|
status: 'stopped',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Delete',
|
||||||
|
iconName: 'trash',
|
||||||
|
type: ['contextmenu', 'inRow'],
|
||||||
|
actionFunc: async (actionDataArg) => {
|
||||||
|
const deployment = actionDataArg.item as plugins.interfaces.data.IDeployment;
|
||||||
|
plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
|
heading: `Delete Deployment`,
|
||||||
|
content: html`
|
||||||
|
<div style="text-align:center">
|
||||||
|
Are you sure you want to delete this deployment?
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;">
|
||||||
|
<div style="color: #fff; font-weight: bold;">
|
||||||
|
${this.getServiceName(deployment.serviceId)}
|
||||||
|
</div>
|
||||||
|
<div style="color: #aaa; font-size: 0.9em; margin-top: 4px;">
|
||||||
|
Node: ${this.getNodeName(deployment.nodeId)}
|
||||||
|
</div>
|
||||||
|
<div style="color: #f44336; margin-top: 8px;">
|
||||||
|
This action cannot be undone.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: 'Cancel',
|
||||||
|
action: async (modalArg) => {
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Delete',
|
||||||
|
action: async (modalArg) => {
|
||||||
|
await appstate.dataState.dispatchAction(appstate.deleteDeploymentAction, {
|
||||||
|
deploymentId: deployment.id,
|
||||||
});
|
});
|
||||||
await modalArg.destroy();
|
await modalArg.destroy();
|
||||||
},
|
},
|
||||||
@@ -127,4 +349,4 @@ export class CloudlyViewDeployments extends DeesElement {
|
|||||||
></dees-table>
|
></dees-table>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,4 +1,3 @@
|
|||||||
import * as plugins from '../plugins.js';
|
|
||||||
import * as shared from '../elements/shared/index.js';
|
import * as shared from '../elements/shared/index.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -34,34 +33,124 @@ export class CloudlyViewOverview extends DeesElement {
|
|||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
shared.viewHostCss,
|
shared.viewHostCss,
|
||||||
css`
|
css`
|
||||||
.clusterGrid {
|
dees-statsgrid {
|
||||||
display: grid;
|
margin-top: 24px;
|
||||||
grid-template-columns: ${cssManager.cssGridColumns(3, 8)};
|
|
||||||
grid-gap: 16px;
|
|
||||||
margin-bottom: 40px;
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
|
// Calculate total nodes across all clusters
|
||||||
|
const totalNodes = this.data.clusters?.reduce((sum, cluster) =>
|
||||||
|
sum + (cluster.data.nodes?.length || 0), 0) || 0;
|
||||||
|
|
||||||
|
// Create tiles for the stats grid
|
||||||
|
const statsTiles = [
|
||||||
|
{
|
||||||
|
id: 'clusters',
|
||||||
|
title: 'Total Clusters',
|
||||||
|
value: this.data.clusters?.length || 0,
|
||||||
|
type: 'number' as const,
|
||||||
|
iconName: 'lucide:Network',
|
||||||
|
description: 'Active clusters'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'nodes',
|
||||||
|
title: 'Total Nodes',
|
||||||
|
value: totalNodes,
|
||||||
|
type: 'number' as const,
|
||||||
|
iconName: 'lucide:Server',
|
||||||
|
description: 'Connected nodes'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'services',
|
||||||
|
title: 'Services',
|
||||||
|
value: this.data.services?.length || 0,
|
||||||
|
type: 'number' as const,
|
||||||
|
iconName: 'lucide:Layers',
|
||||||
|
description: 'Deployed services'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'deployments',
|
||||||
|
title: 'Deployments',
|
||||||
|
value: this.data.deployments?.length || 0,
|
||||||
|
type: 'number' as const,
|
||||||
|
iconName: 'lucide:Rocket',
|
||||||
|
description: 'Active deployments'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'secretGroups',
|
||||||
|
title: 'Secret Groups',
|
||||||
|
value: this.data.secretGroups?.length || 0,
|
||||||
|
type: 'number' as const,
|
||||||
|
iconName: 'lucide:ShieldCheck',
|
||||||
|
description: 'Configured secret groups'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'secretBundles',
|
||||||
|
title: 'Secret Bundles',
|
||||||
|
value: this.data.secretBundles?.length || 0,
|
||||||
|
type: 'number' as const,
|
||||||
|
iconName: 'lucide:LockKeyhole',
|
||||||
|
description: 'Available secret bundles'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'images',
|
||||||
|
title: 'Images',
|
||||||
|
value: this.data.images?.length || 0,
|
||||||
|
type: 'number' as const,
|
||||||
|
iconName: 'lucide:Image',
|
||||||
|
description: 'Container images'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dns',
|
||||||
|
title: 'DNS Zones',
|
||||||
|
value: this.data.dns?.length || 0,
|
||||||
|
type: 'number' as const,
|
||||||
|
iconName: 'lucide:Globe',
|
||||||
|
description: 'Managed DNS zones'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'databases',
|
||||||
|
title: 'Databases',
|
||||||
|
value: this.data.dbs?.length || 0,
|
||||||
|
type: 'number' as const,
|
||||||
|
iconName: 'lucide:Database',
|
||||||
|
description: 'Database instances'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'backups',
|
||||||
|
title: 'Backups',
|
||||||
|
value: this.data.backups?.length || 0,
|
||||||
|
type: 'number' as const,
|
||||||
|
iconName: 'lucide:Save',
|
||||||
|
description: 'Available backups'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mails',
|
||||||
|
title: 'Mail Domains',
|
||||||
|
value: this.data.mails?.length || 0,
|
||||||
|
type: 'number' as const,
|
||||||
|
iconName: 'lucide:Mail',
|
||||||
|
description: 'Mail configurations'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 's3',
|
||||||
|
title: 'S3 Buckets',
|
||||||
|
value: this.data.s3?.length || 0,
|
||||||
|
type: 'number' as const,
|
||||||
|
iconName: 'lucide:Cloud',
|
||||||
|
description: 'Storage buckets'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<cloudly-sectionheading>Overview</cloudly-sectionheading>
|
<cloudly-sectionheading>Overview</cloudly-sectionheading>
|
||||||
${this.data.clusters.length === 0 ? html`
|
<dees-statsgrid
|
||||||
You need to create at least one cluster to see an overview.
|
.tiles=${statsTiles}
|
||||||
`: html``}
|
.minTileWidth=${250}
|
||||||
${this.data.clusters.map(
|
.gap=${16}
|
||||||
(clusterArg) => html`
|
></dees-statsgrid>
|
||||||
<dees-label .label=${'cluster: ' + clusterArg.data.name}></dees-label>
|
|
||||||
<div class="clusterGrid">
|
|
||||||
<dees-chart-area .label=${'System Usage'}></dees-chart-area>
|
|
||||||
<dees-chart-area .label=${'Internet Traffic'}></dees-chart-area>
|
|
||||||
<dees-chart-area .label=${'Requests'}></dees-chart-area>
|
|
||||||
<dees-chart-area .label=${'WebSocket Connections'}></dees-chart-area>
|
|
||||||
<dees-chart-log class="services" .label=${'Deployed Services'}></dees-chart-log>
|
|
||||||
<dees-chart-log class="eventLog" .label=${'Event Log'}></dees-chart-log>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -22,62 +22,187 @@ export class CloudlyViewServices extends DeesElement {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
const subecription = appstate.dataState
|
const subscription = appstate.dataState
|
||||||
.select((stateArg) => stateArg)
|
.select((stateArg) => stateArg)
|
||||||
.subscribe((dataArg) => {
|
.subscribe((dataArg) => {
|
||||||
this.data = dataArg;
|
this.data = dataArg;
|
||||||
});
|
});
|
||||||
this.rxSubscriptions.push(subecription);
|
this.rxSubscriptions.push(subscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
shared.viewHostCss,
|
shared.viewHostCss,
|
||||||
css`
|
css`
|
||||||
|
.category-badge {
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.category-base {
|
||||||
|
background: #2196f3;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.category-distributed {
|
||||||
|
background: #9c27b0;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.category-workload {
|
||||||
|
background: #4caf50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.strategy-badge {
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
background: #444;
|
||||||
|
color: #ccc;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private getCategoryIcon(category: string): string {
|
||||||
|
switch (category) {
|
||||||
|
case 'base':
|
||||||
|
return 'lucide:ServerCog';
|
||||||
|
case 'distributed':
|
||||||
|
return 'lucide:Network';
|
||||||
|
case 'workload':
|
||||||
|
return 'lucide:Container';
|
||||||
|
default:
|
||||||
|
return 'lucide:Box';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCategoryBadgeHtml(category: string): any {
|
||||||
|
const className = `category-badge category-${category}`;
|
||||||
|
return html`<span class="${className}">${category}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getStrategyBadgeHtml(strategy: string): any {
|
||||||
|
return html`<span class="strategy-badge">${strategy}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
return html`
|
return html`
|
||||||
<cloudly-sectionheading>Services</cloudly-sectionheading>
|
<cloudly-sectionheading>Services</cloudly-sectionheading>
|
||||||
<dees-table
|
<dees-table
|
||||||
.heading1=${'Services'}
|
.heading1=${'Services'}
|
||||||
.heading2=${'decoded in client'}
|
.heading2=${'Service configuration and deployment management'}
|
||||||
.data=${this.data.services}
|
.data=${this.data.services || []}
|
||||||
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
|
.displayFunction=${(itemArg: plugins.interfaces.data.IService) => {
|
||||||
return {
|
return {
|
||||||
id: itemArg.id,
|
Name: itemArg.data.name,
|
||||||
serverAmount: itemArg.data.servers.length,
|
Description: itemArg.data.description,
|
||||||
|
Category: this.getCategoryBadgeHtml(itemArg.data.serviceCategory || 'workload'),
|
||||||
|
'Deployment Strategy': html`
|
||||||
|
${this.getStrategyBadgeHtml(itemArg.data.deploymentStrategy || 'custom')}
|
||||||
|
${itemArg.data.maxReplicas ? html`<span style="color: #888; margin-left: 8px;">Max: ${itemArg.data.maxReplicas}</span>` : ''}
|
||||||
|
${itemArg.data.antiAffinity ? html`<span style="color: #f44336; margin-left: 8px;">⚡ Anti-affinity</span>` : ''}
|
||||||
|
`,
|
||||||
|
'Image': `${itemArg.data.imageId}:${itemArg.data.imageVersion}`,
|
||||||
|
'Scale Factor': itemArg.data.scaleFactor,
|
||||||
|
'Balancing': itemArg.data.balancingStrategy,
|
||||||
|
'Deployments': itemArg.data.deploymentIds?.length || 0,
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
.dataActions=${[
|
.dataActions=${[
|
||||||
{
|
{
|
||||||
name: 'add configBundle',
|
name: 'Add Service',
|
||||||
iconName: 'plus',
|
iconName: 'plus',
|
||||||
type: ['header', 'footer'],
|
type: ['header', 'footer'],
|
||||||
actionFunc: async (dataActionArg) => {
|
actionFunc: async (dataActionArg) => {
|
||||||
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
heading: 'Add ConfigBundle',
|
heading: 'Add Service',
|
||||||
content: html`
|
content: html`
|
||||||
<dees-form>
|
<dees-form>
|
||||||
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
<dees-input-text .key=${'name'} .label=${'Service Name'} .required=${true}></dees-input-text>
|
||||||
<dees-input-text
|
<dees-input-text .key=${'description'} .label=${'Description'} .required=${true}></dees-input-text>
|
||||||
.key=${'data.secretGroupIds'}
|
<dees-input-dropdown
|
||||||
.label=${'secretGroupIds'}
|
.key=${'serviceCategory'}
|
||||||
.value=${''}
|
.label=${'Service Category'}
|
||||||
></dees-input-text>
|
.options=${['base', 'distributed', 'workload']}
|
||||||
<dees-input-text
|
.value=${'workload'}
|
||||||
.key=${'data.includedTags'}
|
.required=${true}>
|
||||||
.label=${'includedTags'}
|
</dees-input-dropdown>
|
||||||
.value=${''}
|
<dees-input-dropdown
|
||||||
></dees-input-text>
|
.key=${'deploymentStrategy'}
|
||||||
|
.label=${'Deployment Strategy'}
|
||||||
|
.options=${['all-nodes', 'limited-replicas', 'custom']}
|
||||||
|
.value=${'custom'}
|
||||||
|
.required=${true}>
|
||||||
|
</dees-input-dropdown>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'maxReplicas'}
|
||||||
|
.label=${'Max Replicas (for distributed services)'}
|
||||||
|
.value=${'3'}
|
||||||
|
.type=${'number'}>
|
||||||
|
</dees-input-text>
|
||||||
|
<dees-input-checkbox
|
||||||
|
.key=${'antiAffinity'}
|
||||||
|
.label=${'Enable Anti-Affinity'}
|
||||||
|
.value=${false}>
|
||||||
|
</dees-input-checkbox>
|
||||||
|
<dees-input-text .key=${'imageId'} .label=${'Image ID'} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'imageVersion'} .label=${'Image Version'} .value=${'latest'} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'scaleFactor'}
|
||||||
|
.label=${'Scale Factor'}
|
||||||
|
.value=${'1'}
|
||||||
|
.type=${'number'}
|
||||||
|
.required=${true}>
|
||||||
|
</dees-input-text>
|
||||||
|
<dees-input-dropdown
|
||||||
|
.key=${'balancingStrategy'}
|
||||||
|
.label=${'Balancing Strategy'}
|
||||||
|
.options=${['round-robin', 'least-connections']}
|
||||||
|
.value=${'round-robin'}
|
||||||
|
.required=${true}>
|
||||||
|
</dees-input-dropdown>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'webPort'}
|
||||||
|
.label=${'Web Port'}
|
||||||
|
.value=${'80'}
|
||||||
|
.type=${'number'}
|
||||||
|
.required=${true}>
|
||||||
|
</dees-input-text>
|
||||||
</dees-form>
|
</dees-form>
|
||||||
`,
|
`,
|
||||||
menuOptions: [
|
menuOptions: [
|
||||||
{ name: 'create', action: async (modalArg) => {} },
|
|
||||||
{
|
{
|
||||||
name: 'cancel',
|
name: 'Create Service',
|
||||||
|
action: async (modalArg) => {
|
||||||
|
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
|
||||||
|
const formData = await form.gatherData();
|
||||||
|
|
||||||
|
await appstate.dataState.dispatchAction(appstate.createServiceAction, {
|
||||||
|
serviceData: {
|
||||||
|
name: formData.name,
|
||||||
|
description: formData.description,
|
||||||
|
serviceCategory: formData.serviceCategory,
|
||||||
|
deploymentStrategy: formData.deploymentStrategy,
|
||||||
|
maxReplicas: formData.maxReplicas ? parseInt(formData.maxReplicas) : undefined,
|
||||||
|
antiAffinity: formData.antiAffinity,
|
||||||
|
imageId: formData.imageId,
|
||||||
|
imageVersion: formData.imageVersion,
|
||||||
|
scaleFactor: parseInt(formData.scaleFactor),
|
||||||
|
balancingStrategy: formData.balancingStrategy,
|
||||||
|
ports: {
|
||||||
|
web: parseInt(formData.webPort),
|
||||||
|
},
|
||||||
|
environment: {},
|
||||||
|
domains: [],
|
||||||
|
deploymentIds: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Cancel',
|
||||||
action: async (modalArg) => {
|
action: async (modalArg) => {
|
||||||
modalArg.destroy();
|
modalArg.destroy();
|
||||||
},
|
},
|
||||||
@@ -87,34 +212,137 @@ export class CloudlyViewServices extends DeesElement {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'delete',
|
name: 'Edit',
|
||||||
|
iconName: 'edit',
|
||||||
|
type: ['contextmenu', 'inRow'],
|
||||||
|
actionFunc: async (actionDataArg) => {
|
||||||
|
const service = actionDataArg.item as plugins.interfaces.data.IService;
|
||||||
|
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
|
heading: `Edit Service: ${service.data.name}`,
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text .key=${'name'} .label=${'Service Name'} .value=${service.data.name} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'description'} .label=${'Description'} .value=${service.data.description} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-dropdown
|
||||||
|
.key=${'serviceCategory'}
|
||||||
|
.label=${'Service Category'}
|
||||||
|
.options=${['base', 'distributed', 'workload']}
|
||||||
|
.value=${service.data.serviceCategory || 'workload'}
|
||||||
|
.required=${true}>
|
||||||
|
</dees-input-dropdown>
|
||||||
|
<dees-input-dropdown
|
||||||
|
.key=${'deploymentStrategy'}
|
||||||
|
.label=${'Deployment Strategy'}
|
||||||
|
.options=${['all-nodes', 'limited-replicas', 'custom']}
|
||||||
|
.value=${service.data.deploymentStrategy || 'custom'}
|
||||||
|
.required=${true}>
|
||||||
|
</dees-input-dropdown>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'maxReplicas'}
|
||||||
|
.label=${'Max Replicas (for distributed services)'}
|
||||||
|
.value=${service.data.maxReplicas || ''}
|
||||||
|
.type=${'number'}>
|
||||||
|
</dees-input-text>
|
||||||
|
<dees-input-checkbox
|
||||||
|
.key=${'antiAffinity'}
|
||||||
|
.label=${'Enable Anti-Affinity'}
|
||||||
|
.value=${service.data.antiAffinity || false}>
|
||||||
|
</dees-input-checkbox>
|
||||||
|
<dees-input-text .key=${'imageVersion'} .label=${'Image Version'} .value=${service.data.imageVersion} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'scaleFactor'}
|
||||||
|
.label=${'Scale Factor'}
|
||||||
|
.value=${service.data.scaleFactor}
|
||||||
|
.type=${'number'}
|
||||||
|
.required=${true}>
|
||||||
|
</dees-input-text>
|
||||||
|
<dees-input-dropdown
|
||||||
|
.key=${'balancingStrategy'}
|
||||||
|
.label=${'Balancing Strategy'}
|
||||||
|
.options=${['round-robin', 'least-connections']}
|
||||||
|
.value=${service.data.balancingStrategy}
|
||||||
|
.required=${true}>
|
||||||
|
</dees-input-dropdown>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: 'Update Service',
|
||||||
|
action: async (modalArg) => {
|
||||||
|
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
|
||||||
|
const formData = await form.gatherData();
|
||||||
|
|
||||||
|
await appstate.dataState.dispatchAction(appstate.updateServiceAction, {
|
||||||
|
serviceId: service.id,
|
||||||
|
serviceData: {
|
||||||
|
...service.data,
|
||||||
|
name: formData.name,
|
||||||
|
description: formData.description,
|
||||||
|
serviceCategory: formData.serviceCategory,
|
||||||
|
deploymentStrategy: formData.deploymentStrategy,
|
||||||
|
maxReplicas: formData.maxReplicas ? parseInt(formData.maxReplicas) : undefined,
|
||||||
|
antiAffinity: formData.antiAffinity,
|
||||||
|
imageVersion: formData.imageVersion,
|
||||||
|
scaleFactor: parseInt(formData.scaleFactor),
|
||||||
|
balancingStrategy: formData.balancingStrategy,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Cancel',
|
||||||
|
action: async (modalArg) => {
|
||||||
|
modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Deploy',
|
||||||
|
iconName: 'rocket',
|
||||||
|
type: ['contextmenu', 'inRow'],
|
||||||
|
actionFunc: async (actionDataArg) => {
|
||||||
|
const service = actionDataArg.item as plugins.interfaces.data.IService;
|
||||||
|
// TODO: Implement deployment action
|
||||||
|
console.log('Deploy service:', service);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Delete',
|
||||||
iconName: 'trash',
|
iconName: 'trash',
|
||||||
type: ['contextmenu', 'inRow'],
|
type: ['contextmenu', 'inRow'],
|
||||||
actionFunc: async (actionDataArg) => {
|
actionFunc: async (actionDataArg) => {
|
||||||
|
const service = actionDataArg.item as plugins.interfaces.data.IService;
|
||||||
plugins.deesCatalog.DeesModal.createAndShow({
|
plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
|
heading: `Delete Service: ${service.data.name}`,
|
||||||
content: html`
|
content: html`
|
||||||
<div style="text-align:center">
|
<div style="text-align:center">
|
||||||
Do you really want to delete the ConfigBundle?
|
Are you sure you want to delete this service?
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;">
|
||||||
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
|
<div style="color: #fff; font-weight: bold;">${service.data.name}</div>
|
||||||
>
|
<div style="color: #aaa; font-size: 0.9em; margin-top: 4px;">${service.data.description}</div>
|
||||||
${actionDataArg.item.id}
|
<div style="color: #f44336; margin-top: 8px;">
|
||||||
|
This will also delete ${service.data.deploymentIds?.length || 0} deployment(s)
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
menuOptions: [
|
menuOptions: [
|
||||||
{
|
{
|
||||||
name: 'cancel',
|
name: 'Cancel',
|
||||||
action: async (modalArg) => {
|
action: async (modalArg) => {
|
||||||
await modalArg.destroy();
|
await modalArg.destroy();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'delete',
|
name: 'Delete',
|
||||||
action: async (modalArg) => {
|
action: async (modalArg) => {
|
||||||
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
|
await appstate.dataState.dispatchAction(appstate.deleteServiceAction, {
|
||||||
configBundleId: actionDataArg.item.id,
|
serviceId: service.id,
|
||||||
});
|
});
|
||||||
await modalArg.destroy();
|
await modalArg.destroy();
|
||||||
},
|
},
|
||||||
@@ -127,4 +355,4 @@ export class CloudlyViewServices extends DeesElement {
|
|||||||
></dees-table>
|
></dees-table>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
478
ts_web/elements/cloudly-view-settings.ts
Normal file
478
ts_web/elements/cloudly-view-settings.ts
Normal file
@@ -0,0 +1,478 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import * as shared from '../elements/shared/index.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
state,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
property,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
import * as appstate from '../appstate.js';
|
||||||
|
|
||||||
|
@customElement('cloudly-view-settings')
|
||||||
|
export class CloudlyViewSettings extends DeesElement {
|
||||||
|
@state()
|
||||||
|
private settings: plugins.interfaces.data.ICloudlySettingsMasked = {};
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private isLoading = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private testResults: {[key: string]: {success: boolean; message: string}} = {};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
shared.viewHostCss,
|
||||||
|
css`
|
||||||
|
.settings-container {
|
||||||
|
padding: 24px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-status dees-button {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dees-panel {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid.single {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.form-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
private async loadSettings() {
|
||||||
|
this.isLoading = true;
|
||||||
|
try {
|
||||||
|
const trRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<
|
||||||
|
plugins.interfaces.requests.settings.IRequest_GetSettings
|
||||||
|
>(
|
||||||
|
'/typedrequest',
|
||||||
|
'getSettings'
|
||||||
|
);
|
||||||
|
const response = await trRequest.fire({});
|
||||||
|
this.settings = response.settings;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load settings:', error);
|
||||||
|
plugins.deesCatalog.DeesToast.createAndShow({
|
||||||
|
message: `Failed to load settings: ${error.message}`,
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveSettings(formData: any) {
|
||||||
|
console.log('saveSettings called with formData:', formData);
|
||||||
|
this.isLoading = true;
|
||||||
|
try {
|
||||||
|
const updates: Partial<plugins.interfaces.data.ICloudlySettings> = {};
|
||||||
|
|
||||||
|
// Process form data
|
||||||
|
for (const [key, value] of Object.entries(formData)) {
|
||||||
|
console.log(`Processing ${key}:`, value);
|
||||||
|
if (value !== undefined && value !== '****' && !value?.toString().endsWith('****')) {
|
||||||
|
// Only update if value changed (not masked)
|
||||||
|
updates[key as keyof plugins.interfaces.data.ICloudlySettings] = value as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('Updates to send:', updates);
|
||||||
|
|
||||||
|
const trRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<
|
||||||
|
plugins.interfaces.requests.settings.IRequest_UpdateSettings
|
||||||
|
>(
|
||||||
|
'/typedrequest',
|
||||||
|
'updateSettings'
|
||||||
|
);
|
||||||
|
const response = await trRequest.fire({ updates });
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
plugins.deesCatalog.DeesToast.createAndShow({
|
||||||
|
message: 'Settings saved successfully',
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
await this.loadSettings(); // Reload to get masked values
|
||||||
|
} else {
|
||||||
|
throw new Error(response.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save settings:', error);
|
||||||
|
plugins.deesCatalog.DeesToast.createAndShow({
|
||||||
|
message: `Failed to save settings: ${error.message}`,
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async testConnection(provider: string) {
|
||||||
|
this.isLoading = true;
|
||||||
|
try {
|
||||||
|
const trRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<
|
||||||
|
plugins.interfaces.requests.settings.IRequest_TestProviderConnection
|
||||||
|
>(
|
||||||
|
'/typedrequest',
|
||||||
|
'testProviderConnection'
|
||||||
|
);
|
||||||
|
const response = await trRequest.fire({ provider: provider as any });
|
||||||
|
|
||||||
|
this.testResults = {
|
||||||
|
...this.testResults,
|
||||||
|
[provider]: {
|
||||||
|
success: response.connectionValid,
|
||||||
|
message: response.message
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show toast notification
|
||||||
|
plugins.deesCatalog.DeesToast.createAndShow({
|
||||||
|
message: response.message,
|
||||||
|
type: response.connectionValid ? 'success' : 'error',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.testResults = {
|
||||||
|
...this.testResults,
|
||||||
|
[provider]: {
|
||||||
|
success: false,
|
||||||
|
message: `Test failed: ${error.message}`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
plugins.deesCatalog.DeesToast.createAndShow({
|
||||||
|
message: `Connection test failed: ${error.message}`,
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderProviderStatus(provider: string) {
|
||||||
|
const result = this.testResults[provider];
|
||||||
|
if (!result) return '';
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<dees-badge
|
||||||
|
.type=${result.success ? 'success' : 'error'}
|
||||||
|
.text=${result.success ? 'Connected' : 'Failed'}
|
||||||
|
></dees-badge>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
if (this.isLoading && Object.keys(this.settings).length === 0) {
|
||||||
|
return html`
|
||||||
|
<div class="loading-container">
|
||||||
|
<dees-spinner></dees-spinner>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<cloudly-sectionheading>Settings</cloudly-sectionheading>
|
||||||
|
<div class="settings-container">
|
||||||
|
<dees-form @formData=${(e: CustomEvent) => {
|
||||||
|
console.log('formData event received:', e);
|
||||||
|
console.log('Event detail:', e.detail);
|
||||||
|
console.log('Event detail.data:', e.detail.data);
|
||||||
|
this.saveSettings(e.detail.data);
|
||||||
|
}}>
|
||||||
|
|
||||||
|
<!-- Hetzner Cloud -->
|
||||||
|
<dees-panel
|
||||||
|
.title=${'Hetzner Cloud'}
|
||||||
|
.subtitle=${'Configure Hetzner Cloud API access'}
|
||||||
|
.variant=${'outline'}
|
||||||
|
>
|
||||||
|
<div class="test-status">
|
||||||
|
${this.renderProviderStatus('hetzner')}
|
||||||
|
<dees-button
|
||||||
|
.text=${'Test Connection'}
|
||||||
|
.type=${'secondary'}
|
||||||
|
@click=${(e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.testConnection('hetzner');
|
||||||
|
}}
|
||||||
|
></dees-button>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid single">
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'hetznerToken'}
|
||||||
|
.label=${'API Token'}
|
||||||
|
.value=${this.settings.hetznerToken || ''}
|
||||||
|
.isPasswordBool=${true}
|
||||||
|
.description=${'Your Hetzner Cloud API token for managing infrastructure'}
|
||||||
|
.required=${false}
|
||||||
|
></dees-input-text>
|
||||||
|
</div>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<!-- Cloudflare -->
|
||||||
|
<dees-panel
|
||||||
|
.title=${'Cloudflare'}
|
||||||
|
.subtitle=${'Configure Cloudflare API access'}
|
||||||
|
.variant=${'outline'}
|
||||||
|
>
|
||||||
|
<div class="test-status">
|
||||||
|
${this.renderProviderStatus('cloudflare')}
|
||||||
|
<dees-button
|
||||||
|
.text=${'Test Connection'}
|
||||||
|
.type=${'secondary'}
|
||||||
|
@click=${(e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.testConnection('cloudflare');
|
||||||
|
}}
|
||||||
|
></dees-button>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid single">
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'cloudflareToken'}
|
||||||
|
.label=${'API Token'}
|
||||||
|
.value=${this.settings.cloudflareToken || ''}
|
||||||
|
.isPasswordBool=${true}
|
||||||
|
.description=${'Cloudflare API token with DNS and Zone permissions'}
|
||||||
|
.required=${false}
|
||||||
|
></dees-input-text>
|
||||||
|
</div>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<!-- AWS -->
|
||||||
|
<dees-panel
|
||||||
|
.title=${'Amazon Web Services'}
|
||||||
|
.subtitle=${'Configure AWS credentials'}
|
||||||
|
.variant=${'outline'}
|
||||||
|
>
|
||||||
|
<div class="test-status">
|
||||||
|
${this.renderProviderStatus('aws')}
|
||||||
|
<dees-button
|
||||||
|
.text=${'Test Connection'}
|
||||||
|
.type=${'secondary'}
|
||||||
|
@click=${(e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.testConnection('aws');
|
||||||
|
}}
|
||||||
|
></dees-button>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'awsAccessKey'}
|
||||||
|
.label=${'Access Key ID'}
|
||||||
|
.value=${this.settings.awsAccessKey || ''}
|
||||||
|
.isPasswordBool=${true}
|
||||||
|
.description=${'AWS IAM access key identifier'}
|
||||||
|
.required=${false}
|
||||||
|
></dees-input-text>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'awsSecretKey'}
|
||||||
|
.label=${'Secret Access Key'}
|
||||||
|
.value=${this.settings.awsSecretKey || ''}
|
||||||
|
.isPasswordBool=${true}
|
||||||
|
.description=${'AWS IAM secret access key'}
|
||||||
|
.required=${false}
|
||||||
|
></dees-input-text>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid single">
|
||||||
|
<dees-input-dropdown
|
||||||
|
.key=${'awsRegion'}
|
||||||
|
.label=${'Default Region'}
|
||||||
|
.selectedOption=${this.settings.awsRegion || 'us-east-1'}
|
||||||
|
.options=${[
|
||||||
|
{ key: 'us-east-1', option: 'US East (N. Virginia)', payload: null },
|
||||||
|
{ key: 'us-west-2', option: 'US West (Oregon)', payload: null },
|
||||||
|
{ key: 'eu-west-1', option: 'EU (Ireland)', payload: null },
|
||||||
|
{ key: 'eu-central-1', option: 'EU (Frankfurt)', payload: null },
|
||||||
|
{ key: 'ap-southeast-1', option: 'Asia Pacific (Singapore)', payload: null },
|
||||||
|
{ key: 'ap-northeast-1', option: 'Asia Pacific (Tokyo)', payload: null },
|
||||||
|
]}
|
||||||
|
.description=${'Default AWS region for resource provisioning'}
|
||||||
|
></dees-input-dropdown>
|
||||||
|
</div>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<!-- DigitalOcean -->
|
||||||
|
<dees-panel
|
||||||
|
.title=${'DigitalOcean'}
|
||||||
|
.subtitle=${'Configure DigitalOcean API access'}
|
||||||
|
.variant=${'outline'}
|
||||||
|
>
|
||||||
|
<div class="test-status">
|
||||||
|
${this.renderProviderStatus('digitalocean')}
|
||||||
|
<dees-button
|
||||||
|
.text=${'Test Connection'}
|
||||||
|
.type=${'secondary'}
|
||||||
|
@click=${(e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.testConnection('digitalocean');
|
||||||
|
}}
|
||||||
|
></dees-button>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid single">
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'digitalOceanToken'}
|
||||||
|
.label=${'Personal Access Token'}
|
||||||
|
.value=${this.settings.digitalOceanToken || ''}
|
||||||
|
.isPasswordBool=${true}
|
||||||
|
.description=${'DigitalOcean personal access token with read/write scope'}
|
||||||
|
.required=${false}
|
||||||
|
></dees-input-text>
|
||||||
|
</div>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<!-- Azure -->
|
||||||
|
<dees-panel
|
||||||
|
.title=${'Microsoft Azure'}
|
||||||
|
.subtitle=${'Configure Azure service principal'}
|
||||||
|
.variant=${'outline'}
|
||||||
|
>
|
||||||
|
<div class="test-status">
|
||||||
|
${this.renderProviderStatus('azure')}
|
||||||
|
<dees-button
|
||||||
|
.text=${'Test Connection'}
|
||||||
|
.type=${'secondary'}
|
||||||
|
@click=${(e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.testConnection('azure');
|
||||||
|
}}
|
||||||
|
></dees-button>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'azureClientId'}
|
||||||
|
.label=${'Application (Client) ID'}
|
||||||
|
.value=${this.settings.azureClientId || ''}
|
||||||
|
.isPasswordBool=${true}
|
||||||
|
.description=${'Azure AD application client ID'}
|
||||||
|
.required=${false}
|
||||||
|
></dees-input-text>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'azureClientSecret'}
|
||||||
|
.label=${'Client Secret'}
|
||||||
|
.value=${this.settings.azureClientSecret || ''}
|
||||||
|
.isPasswordBool=${true}
|
||||||
|
.description=${'Azure AD application client secret'}
|
||||||
|
.required=${false}
|
||||||
|
></dees-input-text>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'azureTenantId'}
|
||||||
|
.label=${'Directory (Tenant) ID'}
|
||||||
|
.value=${this.settings.azureTenantId || ''}
|
||||||
|
.description=${'Azure AD tenant identifier'}
|
||||||
|
.required=${false}
|
||||||
|
></dees-input-text>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'azureSubscriptionId'}
|
||||||
|
.label=${'Subscription ID'}
|
||||||
|
.value=${this.settings.azureSubscriptionId || ''}
|
||||||
|
.description=${'Azure subscription for resource management'}
|
||||||
|
.required=${false}
|
||||||
|
></dees-input-text>
|
||||||
|
</div>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<!-- Google Cloud -->
|
||||||
|
<dees-panel
|
||||||
|
.title=${'Google Cloud Platform'}
|
||||||
|
.subtitle=${'Configure GCP service account'}
|
||||||
|
.variant=${'outline'}
|
||||||
|
>
|
||||||
|
<div class="test-status">
|
||||||
|
${this.renderProviderStatus('google')}
|
||||||
|
<dees-button
|
||||||
|
.text=${'Test Connection'}
|
||||||
|
.type=${'secondary'}
|
||||||
|
@click=${(e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.testConnection('google');
|
||||||
|
}}
|
||||||
|
></dees-button>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid single">
|
||||||
|
<dees-input-textarea
|
||||||
|
.key=${'googleCloudKeyJson'}
|
||||||
|
.label=${'Service Account Key (JSON)'}
|
||||||
|
.value=${this.settings.googleCloudKeyJson || ''}
|
||||||
|
.isPasswordBool=${true}
|
||||||
|
.description=${'Complete JSON key file for service account authentication'}
|
||||||
|
.required=${false}
|
||||||
|
></dees-input-textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-grid single">
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'googleCloudProjectId'}
|
||||||
|
.label=${'Project ID'}
|
||||||
|
.value=${this.settings.googleCloudProjectId || ''}
|
||||||
|
.description=${'Google Cloud project identifier'}
|
||||||
|
.required=${false}
|
||||||
|
></dees-input-text>
|
||||||
|
</div>
|
||||||
|
</dees-panel>
|
||||||
|
|
||||||
|
<div class="actions-container">
|
||||||
|
<dees-form-submit
|
||||||
|
.text=${'Save All Settings'}
|
||||||
|
.disabled=${this.isLoading}
|
||||||
|
></dees-form-submit>
|
||||||
|
</div>
|
||||||
|
</dees-form>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user