Compare commits
29 Commits
Author | SHA1 | Date | |
---|---|---|---|
c142519004 | |||
54ef62e7af | |||
83abe37d8c | |||
eefaa55e13 | |||
330797ab1a | |||
4b3b91312b | |||
1580bb1585 | |||
af7fcf6c2e | |||
23c9e3f678 | |||
7d4e766e9e | |||
907f3e8320 | |||
bc7a2ca5f1 | |||
77d911e47a | |||
b9c9c2d0a9 | |||
d5b91789d1 | |||
eb8350f453 | |||
b987ce27b8 | |||
630e363e53 | |||
a602021952 | |||
80585437a0 | |||
4674a20a2c | |||
820cdfcd48 | |||
6e5dd9b05a | |||
f3d5c21fab | |||
04b278ee28 | |||
7084d76c43 | |||
41d7550e89 | |||
4bf361d3a6 | |||
d70617a90c |
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"
|
104
changelog.md
104
changelog.md
@@ -1,5 +1,109 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
Fix Coreflow identity lookup and response shape; improve API client tests and bump dependencies
|
||||||
|
|
||||||
|
- ts/manager.coreflow/coreflowmanager.ts: Use $elemMatch to correctly query nested user.tokens when resolving identities and validate machine user types.
|
||||||
|
- ts/manager.coreflow/coreflowmanager.ts: Normalize getClusterConfig response to include services (was deploymentDirectives) and tidy handler signatures.
|
||||||
|
- test/test.apiclient.ts: Add detailed logging and improved error handling across preTask, client startup, identity retrieval, image creation and image upload to aid debugging and test observability.
|
||||||
|
- package.json: Update dependency versions (notable bumps): @types/node -> ^22.0.0, @push.rocks/smartacme -> ^8.0.0, @push.rocks/smartdata -> ^5.16.4, @push.rocks/smartexpect -> ^2.5.0, @push.rocks/smartpath -> ^6.0.0, @push.rocks/smartrequest -> ^4.2.2, plus other maintenance bumps.
|
||||||
|
- Add .claude/settings.local.json to provide local Claude permissions for developer tooling.
|
||||||
|
|
||||||
|
## 2025-04-25 - 5.0.4 - fix(platformservice/mta)
|
||||||
|
Update getEmailStatus response schema: make details property optional
|
||||||
|
|
||||||
|
- Changed details property from required with fixed message to optional with a flexible message structure in IReq_GetEMailStats response
|
||||||
|
|
||||||
|
## 2025-04-25 - 5.0.3 - fix(mta)
|
||||||
|
update email status response type in MTA platform service
|
||||||
|
|
||||||
|
- Changed the response 'status' field in IRequest_CheckEmailStatus from a literal 'unknown' to a generic string for improved flexibility
|
||||||
|
|
||||||
|
## 2025-04-25 - 5.0.2 - fix(platformservice/mta)
|
||||||
|
Refactor email status response in MTA service
|
||||||
|
|
||||||
|
- Updated IReq_CheckEmailStatus response: replaced union type ('ok' | 'not ok') with fixed status 'unknown' and added a details object with message 'Email not found'.
|
||||||
|
|
||||||
|
## 2025-04-25 - 5.0.1 - fix(mta)
|
||||||
|
Update email stats response interface in mta platform service to include totalEmailsSent, totalEmailsDelivered, totalEmailsBounced, averageDeliveryTimeMs, and lastUpdated timestamp.
|
||||||
|
|
||||||
|
- Modified IReq_GetEMailStats response in ts_interfaces/platformservice/mta.ts from an empty status object to a detailed email statistics structure.
|
||||||
|
|
||||||
|
## 2025-04-25 - 5.0.0 - BREAKING CHANGE(ts_interfaces/platformservice/mta)
|
||||||
|
Rename mta interfaces and upgrade dependency versions
|
||||||
|
|
||||||
|
- Upgraded devDependencies: @git.zone/tsbuild, tsbundle, tsdoc, tstest, tswatch, and @push.rocks/tapbundle to newer versions.
|
||||||
|
- Upgraded dependencies: @design.estate/dees-catalog, dees-domtools, dees-element, @push.rocks/smartdata, smartexpect, smartfile, smartpromise, smartrequest, smartrx, and tsclass (v4.2.0 to v9.0.0).
|
||||||
|
- Added new packageManager field in package.json and introduced pnpm-workspace.yaml for additional workspace configuration.
|
||||||
|
- Refactored mta API interfaces: renamed IRequest_SendEmail to IReq_SendEmail and IRequestRegisterRecipient to IReq_RegisterRecipient; added IReq_CheckEmailStatus and IReq_GetEMailStats.
|
||||||
|
|
||||||
|
## 2025-01-20 - 4.13.0 - feat(service)
|
||||||
|
Add support for service creation, update, and deletion.
|
||||||
|
|
||||||
|
- Implemented TypedHandlers for creating a new service.
|
||||||
|
- Added features to update existing service details.
|
||||||
|
- Enabled deletion of services by their unique ID.
|
||||||
|
|
||||||
|
## 2025-01-20 - 4.12.2 - fix(service)
|
||||||
|
Fix secret bundle and service management bugs
|
||||||
|
|
||||||
|
- Corrected the field name from 'includedImages' to 'imageClaims' in secret bundles.
|
||||||
|
- Implemented 'getFlatKeyValueObject' for secret bundles and modified related API interactions.
|
||||||
|
- Enhanced the Service class with methods for handling secret bundle data by resolving related groups and environments.
|
||||||
|
|
||||||
|
## 2025-01-02 - 4.12.1 - fix(deps)
|
||||||
|
Updated @git.zone/tspublish to version ^1.9.1
|
||||||
|
|
||||||
|
|
||||||
|
## 2025-01-02 - 4.12.0 - feat(cli)
|
||||||
|
Add CLI support and external registries view
|
||||||
|
|
||||||
|
- Adds CLI client functionality
|
||||||
|
- Introduces a new view for External Registries in the dashboard
|
||||||
|
|
||||||
## 2024-12-30 - 4.11.0 - feat(external-registry)
|
## 2024-12-30 - 4.11.0 - feat(external-registry)
|
||||||
Introduce external registry management
|
Introduce external registry management
|
||||||
|
|
||||||
|
64
package.json
64
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/cloudly",
|
"name": "@serve.zone/cloudly",
|
||||||
"version": "4.11.0",
|
"version": "5.2.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",
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"author": "Task Venture Capital GmbH",
|
"author": "Task Venture Capital GmbH",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "(tstest test/)",
|
"test": "(tstest test/ --verbose --logfile --timeout 120)",
|
||||||
"build": "tsbuild tsfolders --web --allowimplicitany && tsbundle website --production",
|
"build": "tsbuild tsfolders --web --allowimplicitany && tsbundle website --production",
|
||||||
"start": "node cli.js",
|
"start": "node cli.js",
|
||||||
"startTs": "node cli.ts.js",
|
"startTs": "node cli.ts.js",
|
||||||
@@ -22,59 +22,58 @@
|
|||||||
"docs": "tsdoc aidoc"
|
"docs": "tsdoc aidoc"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.2.0",
|
"@git.zone/tsbuild": "^2.6.8",
|
||||||
"@git.zone/tsbundle": "^2.1.0",
|
"@git.zone/tsbundle": "^2.5.1",
|
||||||
"@git.zone/tsdoc": "^1.4.2",
|
"@git.zone/tsdoc": "^1.5.2",
|
||||||
"@git.zone/tspublish": "^1.7.7",
|
"@git.zone/tspublish": "^1.10.3",
|
||||||
"@git.zone/tstest": "^1.0.90",
|
"@git.zone/tstest": "^2.3.6",
|
||||||
"@git.zone/tswatch": "^2.0.37",
|
"@git.zone/tswatch": "^2.2.1",
|
||||||
"@push.rocks/tapbundle": "^5.5.3",
|
"@types/node": "^22.0.0"
|
||||||
"@types/node": "^22.10.2"
|
|
||||||
},
|
},
|
||||||
"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.51",
|
"@api.global/typedserver": "^3.0.79",
|
||||||
"@api.global/typedsocket": "^3.0.1",
|
"@api.global/typedsocket": "^3.0.1",
|
||||||
"@apiclient.xyz/cloudflare": "^6.0.1",
|
"@apiclient.xyz/cloudflare": "^6.4.1",
|
||||||
"@apiclient.xyz/docker": "^1.2.7",
|
"@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.3.2",
|
"@design.estate/dees-catalog": "^1.11.2",
|
||||||
"@design.estate/dees-domtools": "^2.0.64",
|
"@design.estate/dees-domtools": "^2.3.3",
|
||||||
"@design.estate/dees-element": "^2.0.39",
|
"@design.estate/dees-element": "^2.1.2",
|
||||||
"@git.zone/tsrun": "^1.3.3",
|
"@git.zone/tsrun": "^1.3.3",
|
||||||
"@push.rocks/early": "^4.0.3",
|
"@push.rocks/early": "^4.0.3",
|
||||||
"@push.rocks/npmextra": "^5.1.2",
|
"@push.rocks/npmextra": "^5.3.3",
|
||||||
"@push.rocks/projectinfo": "^5.0.1",
|
"@push.rocks/projectinfo": "^5.0.1",
|
||||||
"@push.rocks/qenv": "^6.1.0",
|
"@push.rocks/qenv": "^6.1.3",
|
||||||
"@push.rocks/smartacme": "^5.0.0",
|
"@push.rocks/smartacme": "^8.0.0",
|
||||||
"@push.rocks/smartbucket": "^3.3.7",
|
"@push.rocks/smartbucket": "^3.3.10",
|
||||||
"@push.rocks/smartcli": "^4.0.11",
|
"@push.rocks/smartcli": "^4.0.11",
|
||||||
"@push.rocks/smartclickhouse": "^2.0.17",
|
"@push.rocks/smartclickhouse": "^2.0.17",
|
||||||
"@push.rocks/smartdata": "^5.2.10",
|
"@push.rocks/smartdata": "^5.16.4",
|
||||||
"@push.rocks/smartdelay": "^3.0.5",
|
"@push.rocks/smartdelay": "^3.0.5",
|
||||||
"@push.rocks/smartexit": "^1.0.23",
|
"@push.rocks/smartexit": "^1.0.23",
|
||||||
"@push.rocks/smartexpect": "^1.2.1",
|
"@push.rocks/smartexpect": "^2.5.0",
|
||||||
"@push.rocks/smartfile": "^11.0.23",
|
"@push.rocks/smartfile": "^11.2.7",
|
||||||
"@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.0.7",
|
"@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": "^5.0.18",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartpromise": "^4.0.4",
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
"@push.rocks/smartrequest": "^2.0.23",
|
"@push.rocks/smartrequest": "^4.3.1",
|
||||||
"@push.rocks/smartrx": "^3.0.7",
|
"@push.rocks/smartrx": "^3.0.10",
|
||||||
"@push.rocks/smartssh": "^2.0.1",
|
"@push.rocks/smartssh": "^2.0.1",
|
||||||
"@push.rocks/smartstate": "^2.0.19",
|
"@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": "^4.2.0"
|
"@tsclass/tsclass": "^9.2.0"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"ts/**/*",
|
"ts/**/*",
|
||||||
@@ -126,5 +125,6 @@
|
|||||||
"frontend",
|
"frontend",
|
||||||
"backend",
|
"backend",
|
||||||
"security"
|
"security"
|
||||||
]
|
],
|
||||||
|
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
|
||||||
}
|
}
|
||||||
|
10911
pnpm-lock.yaml
generated
10911
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
4
pnpm-workspace.yaml
Normal file
4
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
onlyBuiltDependencies:
|
||||||
|
- esbuild
|
||||||
|
- mongodb-memory-server
|
||||||
|
- puppeteer
|
@@ -9,3 +9,11 @@
|
|||||||
|
|
||||||
- Note: the exports are defined in the package.json.
|
- Note: the exports are defined in the package.json.
|
||||||
- For now, cloud wise only the setup with cloudron and hetzner cloud is supported.
|
- For now, cloud wise only the setup with cloudron and hetzner cloud is supported.
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
- serve.zone is a monorepo containing multiple packages that work together to provide a complete container orchestration platform
|
||||||
|
- Uses Docker Swarm as the underlying container orchestration technology
|
||||||
|
- cloudly acts as the control plane providing API, web UI, and CLI interfaces
|
||||||
|
- coreflow runs inside Docker Swarm clusters to manage containers
|
||||||
|
- coretraffic runs on each node to handle traffic routing and SSL
|
||||||
|
- spark manages individual servers at the OS level
|
443
readme.md
443
readme.md
@@ -1,335 +1,242 @@
|
|||||||
# @serve.zone/cloudly
|
# @serve.zone/cloudly 🚀
|
||||||
|
|
||||||
A multi-cloud management tool utilizing Docker Swarmkit for orchestrating containerized apps across various cloud providers, with web, CLI, and API interfaces for configuration and integration management.
|
**Multi-cloud orchestration made simple.** Manage containerized applications across cloud providers with Docker Swarmkit, featuring web dashboards, CLI tools, and powerful APIs.
|
||||||
|
|
||||||
## Install
|
## 🎯 What is Cloudly?
|
||||||
|
|
||||||
To install `@serve.zone/cloudly`, run the following command in your terminal:
|
Cloudly is your command center for multi-cloud infrastructure. It abstracts away the complexity of managing resources across different cloud providers while giving you the power and flexibility you need for modern DevOps workflows.
|
||||||
|
|
||||||
|
### ✨ Key Features
|
||||||
|
|
||||||
|
- **🌐 Multi-Cloud Management** - Seamlessly orchestrate resources across Cloudflare, Hetzner, DigitalOcean and more
|
||||||
|
- **🐳 Docker Swarmkit Integration** - Native container orchestration with production-grade reliability
|
||||||
|
- **🔐 Secret Management** - Secure handling of credentials, API keys, and sensitive configuration
|
||||||
|
- **🎨 Web Dashboard** - Beautiful, responsive UI built with modern web components
|
||||||
|
- **⚡ CLI & API** - Full programmatic control through TypeScript/JavaScript APIs and command-line tools
|
||||||
|
- **🔒 SSL/TLS Automation** - Automatic certificate provisioning via Let's Encrypt
|
||||||
|
- **📊 Comprehensive Logging** - Built-in log aggregation and monitoring capabilities
|
||||||
|
- **🔄 Task Scheduling** - Automated workflows and background job management
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install @serve.zone/cloudly --save
|
# Install the main package
|
||||||
|
pnpm add @serve.zone/cloudly
|
||||||
|
|
||||||
|
# Or install the CLI globally
|
||||||
|
pnpm add -g @serve.zone/cli
|
||||||
|
|
||||||
|
# Or just the API client
|
||||||
|
pnpm add @serve.zone/api
|
||||||
```
|
```
|
||||||
|
|
||||||
This will install the package and add it to your project's `package.json` dependencies.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
`@serve.zone/cloudly` is designed to provide a unified interface for managing multi-cloud environments, encapsulating complex cloud interactions with Docker Swarmkit into simpler, programmable entities. This document will guide you through various use-cases and implementation examples to give you a comprehensive understanding of the module's capabilities.
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
Before you begin, ensure your environment is set up correctly:
|
|
||||||
- You have Node.js installed (preferably the latest LTS version).
|
|
||||||
- Your environment is configured to use TypeScript if you're working in a TypeScript project.
|
|
||||||
|
|
||||||
### Basic Setup
|
### Basic Setup
|
||||||
|
|
||||||
#### Creating a Cloudly Instance
|
|
||||||
|
|
||||||
The foundation of working with `@serve.zone/cloudly` involves creating an instance of the `Cloudly` class. This instance serves as the gateway to managing cloud resources and orchestrates interactions within the platform. Here’s how to get started:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Cloudly, ICloudlyConfig } from '@serve.zone/cloudly';
|
|
||||||
|
|
||||||
const myCloudlyConfig: ICloudlyConfig = {
|
|
||||||
cfToken: 'your_cloudflare_api_token',
|
|
||||||
hetznerToken: 'your_hetzner_api_token',
|
|
||||||
environment: 'development',
|
|
||||||
letsEncryptEmail: 'lets_encrypt_email@example.com',
|
|
||||||
publicUrl: 'example.com',
|
|
||||||
publicPort: '8443',
|
|
||||||
mongoDescriptor: {
|
|
||||||
mongoDbUrl: 'mongodb+srv://<username>:<password>@<cluster>.mongodb.net/myFirstDatabase',
|
|
||||||
mongoDbName: 'myDatabase',
|
|
||||||
mongoDbUser: 'myUser',
|
|
||||||
mongoDbPass: 'myPassword',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const myCloudlyInstance = new Cloudly(myCloudlyConfig);
|
|
||||||
```
|
|
||||||
|
|
||||||
The configuration object `ICloudlyConfig` provides essential information needed for initializing external services, such as Cloudflare, Hetzner, and a MongoDB server. Adjust the parameters to match your actual service credentials and specifications.
|
|
||||||
|
|
||||||
### Core Features and Use Cases
|
|
||||||
|
|
||||||
#### Orchestrating Docker Swarmkit Clusters
|
|
||||||
|
|
||||||
Docker Swarmkit cluster management is a primary feature of `@serve.zone/cloudly`. Through its abstracted, programmable interface, you can operate clusters effortlessly. Here’s an example of how to create a cluster using `Cloudly`:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Cloudly } from '@serve.zone/cloudly';
|
import { Cloudly } from '@serve.zone/cloudly';
|
||||||
|
|
||||||
interface ICluster {
|
// Initialize Cloudly with your configuration
|
||||||
name: string;
|
const cloudly = new Cloudly({
|
||||||
id: string;
|
cfToken: process.env.CLOUDFLARE_TOKEN,
|
||||||
cloudlyUrl: string;
|
hetznerToken: process.env.HETZNER_TOKEN,
|
||||||
servers: string[];
|
environment: 'production',
|
||||||
sshKeys: string[];
|
letsEncryptEmail: 'certs@example.com',
|
||||||
}
|
publicUrl: 'cloudly.example.com',
|
||||||
|
publicPort: 443,
|
||||||
async function manageClusters() {
|
|
||||||
const myCloudlyConfig = {
|
|
||||||
cfToken: 'your_cloudflare_api_token',
|
|
||||||
environment: 'development',
|
|
||||||
mongoDescriptor: {
|
mongoDescriptor: {
|
||||||
mongoDbUrl: 'mongodb+srv://<username>:<password>@<cluster>.mongodb.net/myFirstDatabase',
|
mongoDbUrl: process.env.MONGODB_URL,
|
||||||
mongoDbName: 'myDatabase',
|
mongoDbName: 'cloudly',
|
||||||
mongoDbUser: 'myUser',
|
mongoDbUser: process.env.MONGODB_USER,
|
||||||
mongoDbPass: 'myPassword',
|
mongoDbPass: process.env.MONGODB_PASS,
|
||||||
},
|
|
||||||
letsEncryptEmail: 'lets_encrypt_email@example.com',
|
|
||||||
publicUrl: 'example.com',
|
|
||||||
publicPort: 8443,
|
|
||||||
hetznerToken: 'your_hetzner_api_token',
|
|
||||||
};
|
|
||||||
|
|
||||||
const myCloudlyInstance = new Cloudly(myCloudlyConfig);
|
|
||||||
await myCloudlyInstance.start();
|
|
||||||
|
|
||||||
const newCluster: ICluster = {
|
|
||||||
name: 'example_cluster',
|
|
||||||
id: 'example_cluster_id',
|
|
||||||
cloudlyUrl: 'https://example.com:8443',
|
|
||||||
servers: [],
|
|
||||||
sshKeys: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Store the newly created cluster with Cloudly
|
|
||||||
const storedCluster = await myCloudlyInstance.clusterManager.storeCluster(newCluster);
|
|
||||||
console.log('Cluster stored:', storedCluster);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
manageClusters();
|
// Start the platform
|
||||||
|
await cloudly.start();
|
||||||
|
console.log('🎉 Cloudly is running!');
|
||||||
```
|
```
|
||||||
|
|
||||||
In this scenario, a cluster called `example_cluster` is initialized using the `Cloudly` instance. This method represents a central mechanism to efficiently handle cluster entities and associated metadata.
|
## 🏗️ Architecture
|
||||||
|
|
||||||
#### Integrating With Cloudflare for DNS Management
|
Cloudly follows a modular architecture with clear separation of concerns:
|
||||||
|
|
||||||
`@serve.zone/cloudly` provides built-in capabilities for managing DNS records through integration with Cloudflare. Using the `CloudflareConnector`, you can programmatically create, manage, and delete DNS entries:
|
```
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ Web Dashboard (UI) │
|
||||||
|
├─────────────────────────────────────────────┤
|
||||||
|
│ API Layer (TypedRouter) │
|
||||||
|
├─────────────────────────────────────────────┤
|
||||||
|
│ Core Managers │
|
||||||
|
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||||||
|
│ │ Cluster │ │ Image │ │ Secret │ ... │
|
||||||
|
│ └─────────┘ └─────────┘ └─────────┘ │
|
||||||
|
├─────────────────────────────────────────────┤
|
||||||
|
│ Cloud Connectors │
|
||||||
|
│ ┌──────────┐ ┌─────────┐ ┌──────────----┐ │
|
||||||
|
│ │Cloudflare│ │ Hetzner │ │ DigitalOcean │ │
|
||||||
|
│ └──────────┘ └─────────┘ └──────────----┘ │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💻 Core Components
|
||||||
|
|
||||||
|
### 🔧 Managers
|
||||||
|
|
||||||
|
- **AuthManager** - Identity and access management
|
||||||
|
- **ClusterManager** - Docker Swarm cluster orchestration
|
||||||
|
- **ImageManager** - Container image lifecycle management
|
||||||
|
- **SecretManager** - Secure credential storage and distribution
|
||||||
|
- **ServerManager** - Cloud server provisioning and management
|
||||||
|
- **TaskManager** - Background job scheduling and execution
|
||||||
|
|
||||||
|
### 🔌 Connectors
|
||||||
|
|
||||||
|
- **CloudflareConnector** - DNS, CDN, and edge services
|
||||||
|
- **LetsencryptConnector** - Automatic SSL certificate provisioning
|
||||||
|
- **MongodbConnector** - Database persistence layer
|
||||||
|
- **HetznerConnector** - German cloud infrastructure
|
||||||
|
- **DigitalOceanConnector** - Developer-friendly cloud resources
|
||||||
|
|
||||||
|
## 📚 Usage Examples
|
||||||
|
|
||||||
|
### Managing Clusters
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Cloudly } from '@serve.zone/cloudly';
|
// Create a new cluster
|
||||||
|
const cluster = await cloudly.clusterManager.createCluster({
|
||||||
|
name: 'production-cluster',
|
||||||
|
region: 'eu-central',
|
||||||
|
nodeCount: 3
|
||||||
|
});
|
||||||
|
|
||||||
async function configureCloudflareDNS() {
|
// Deploy a service
|
||||||
const myCloudlyConfig = {
|
await cluster.deployService({
|
||||||
cfToken: 'your_cloudflare_api_token',
|
name: 'api-service',
|
||||||
environment: 'development',
|
image: 'myapp:latest',
|
||||||
letsEncryptEmail: 'lets_encrypt_email@example.com',
|
replicas: 3,
|
||||||
publicUrl: 'example.com',
|
ports: [{ published: 80, target: 3000 }]
|
||||||
publicPort: 8443,
|
});
|
||||||
hetznerToken: 'your_hetzner_api_token',
|
|
||||||
mongoDescriptor: {
|
|
||||||
mongoDbUrl: 'mongodb+srv://<username>:<password>@<cluster>.mongodb.net/myFirstDatabase',
|
|
||||||
mongoDbName: 'myDatabase',
|
|
||||||
mongoDbUser: 'myUser',
|
|
||||||
mongoDbPass: 'myPassword',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const myCloudlyInstance = new Cloudly(myCloudlyConfig);
|
|
||||||
await myCloudlyInstance.start();
|
|
||||||
|
|
||||||
const cfConnector = myCloudlyInstance.cloudflareConnector.cloudflare;
|
|
||||||
|
|
||||||
const dnsRecord = await cfConnector.createDNSRecord('example.com', 'sub.example.com', 'A', '127.0.0.1');
|
|
||||||
console.log('DNS Record:', dnsRecord);
|
|
||||||
}
|
|
||||||
|
|
||||||
configureCloudflareDNS();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Here, you create an A record for the subdomain `sub.example.com` pointing to `127.0.0.1`. All communication with Cloudflare is handled directly through the interface without manual intervention.
|
### Secret Management
|
||||||
|
|
||||||
#### Dynamic Interaction with DigitalOcean
|
|
||||||
|
|
||||||
DigitalOcean resource management, including droplet creation, is simplified in Cloudly. By extending the API to encapsulate calls to external providers, Cloudly provides a seamless experience:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Cloudly } from '@serve.zone/cloudly';
|
// Create a secret group
|
||||||
|
const secretGroup = await cloudly.secretManager.createSecretGroup({
|
||||||
|
name: 'api-credentials',
|
||||||
|
secrets: [
|
||||||
|
{ key: 'API_KEY', value: process.env.API_KEY },
|
||||||
|
{ key: 'DB_PASSWORD', value: process.env.DB_PASSWORD }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
async function createDigitalOceanDroplets() {
|
// Create a bundle for deployment
|
||||||
const myCloudlyConfig = {
|
const bundle = await cloudly.secretManager.createSecretBundle({
|
||||||
cfToken: 'your_cloudflare_api_token',
|
name: 'production-secrets',
|
||||||
environment: 'development',
|
secretGroups: [secretGroup]
|
||||||
letsEncryptEmail: 'lets_encrypt_email@example.com',
|
});
|
||||||
publicUrl: 'example.com',
|
|
||||||
publicPort: 8443,
|
|
||||||
hetznerToken: 'your_hetzner_api_token',
|
|
||||||
mongoDescriptor: {
|
|
||||||
mongoDbUrl: 'mongodb+srv://<username>:<password>@<cluster>.mongodb.net/myFirstDatabase',
|
|
||||||
mongoDbName: 'myDatabase',
|
|
||||||
mongoDbUser: 'myUser',
|
|
||||||
mongoDbPass: 'myPassword',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const myCloudlyInstance = new Cloudly(myCloudlyConfig);
|
|
||||||
await myCloudlyInstance.start();
|
|
||||||
|
|
||||||
const doConnector = myCloudlyInstance.digitaloceanConnector;
|
|
||||||
|
|
||||||
const droplet = await doConnector.createDroplet('example-droplet', 'nyc3', 's-1vcpu-1gb', 'ubuntu-20-04-x64');
|
|
||||||
console.log('Droplet created:', droplet);
|
|
||||||
}
|
|
||||||
|
|
||||||
createDigitalOceanDroplets();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
In this script, a droplet named `example-droplet` is created within the `nyc3` region using the `ubuntu-20-04-x64` image. The module abstracts complexities by directly interfacing with DigitalOcean.
|
### DNS Management
|
||||||
|
|
||||||
### Advanced Use Cases
|
```typescript
|
||||||
|
// Create DNS records via Cloudflare
|
||||||
|
const record = await cloudly.cloudflareConnector.createDNSRecord(
|
||||||
|
'example.com',
|
||||||
|
'api.example.com',
|
||||||
|
'A',
|
||||||
|
'192.168.1.1'
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
#### Implementing Web Management Interface
|
### Web Dashboard
|
||||||
|
|
||||||
`@serve.zone/cloudly` facilitates dashboard management with advanced Web Components built with `@design.estate`. This section of the library allows the creation of dynamic, interactive panels for real-time resource management in a modern browser interface.
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { html } from '@design.estate/dees-element';
|
import { html } from '@design.estate/dees-element';
|
||||||
|
|
||||||
const renderDashboard = () => {
|
// Create a custom dashboard view
|
||||||
return html`
|
const dashboard = html`
|
||||||
<cloudly-dashboard>
|
<cloudly-dashboard>
|
||||||
<dees-simple-appdash>
|
|
||||||
<!-- Define sections and elements -->
|
|
||||||
<cloudly-view-clusters></cloudly-view-clusters>
|
<cloudly-view-clusters></cloudly-view-clusters>
|
||||||
<cloudly-view-dns></cloudly-view-dns>
|
<cloudly-view-dns></cloudly-view-dns>
|
||||||
<cloudly-view-images></cloudly-view-images>
|
<cloudly-view-images></cloudly-view-images>
|
||||||
<!-- Other custom views -->
|
|
||||||
</dees-simple-appdash>
|
|
||||||
</cloudly-dashboard>
|
</cloudly-dashboard>
|
||||||
`;
|
`;
|
||||||
};
|
|
||||||
|
|
||||||
document.body.appendChild(renderDashboard());
|
document.body.appendChild(dashboard);
|
||||||
```
|
```
|
||||||
|
|
||||||
Utilizing the custom web components designed specifically for Cloudly, dashboards are adaptable, interactive, and maintainable. These elements allow you to structure a complete cloud management center without needing to delve into detailed UI engineering.
|
## 🛠️ CLI Usage
|
||||||
|
|
||||||
#### Comprehensive Log Management
|
The CLI provides quick access to all Cloudly features:
|
||||||
|
|
||||||
With Cloudly’s Log Management capabilities, you can track and analyze system logs for better insights into your cloud ecosystem’s behavior:
|
```bash
|
||||||
|
# Login to your Cloudly instance
|
||||||
|
servezone login --url https://cloudly.example.com
|
||||||
|
|
||||||
```typescript
|
# List clusters
|
||||||
import { Cloudly } from '@serve.zone/cloudly';
|
servezone clusters list
|
||||||
|
|
||||||
async function initiateLogManagement() {
|
# Deploy a service
|
||||||
const myCloudlyConfig = {
|
servezone deploy --cluster prod --image myapp:latest
|
||||||
cfToken: 'your_cloudflare_api_token',
|
|
||||||
environment: 'development',
|
|
||||||
letsEncryptEmail: 'lets_encrypt_email@example.com',
|
|
||||||
publicUrl: 'example.com',
|
|
||||||
publicPort: 8443,
|
|
||||||
hetznerToken: 'your_hetzner_api_token',
|
|
||||||
mongoDescriptor: {
|
|
||||||
mongoDbUrl: 'mongodb+srv://<username>:<password>@<cluster>.mongodb.net/myFirstDatabase',
|
|
||||||
mongoDbName: 'myDatabase',
|
|
||||||
mongoDbUser: 'myUser',
|
|
||||||
mongoDbPass: 'myPassword',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const myCloudlyInstance = new Cloudly(myCloudlyConfig);
|
# Manage secrets
|
||||||
await myCloudlyInstance.start();
|
servezone secrets create --name api-key --value "secret123"
|
||||||
|
|
||||||
const logs = await myCloudlyInstance.logManager.fetchLogs();
|
# View logs
|
||||||
console.log('Logs:', logs);
|
servezone logs --service api-service --follow
|
||||||
}
|
|
||||||
|
|
||||||
initiateLogManagement();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Cloudly provides the tools needed to collect and process logs within your cloud infrastructure. Logs are an essential part of system validation, troubleshooting, monitoring, and auditing.
|
## 📦 Package Exports
|
||||||
|
|
||||||
#### Secret Management and Bundles
|
This monorepo publishes multiple packages:
|
||||||
|
|
||||||
Managing secrets securely and efficiently is critical for cloud operations. Cloudly allows you to create and manage secret groups and bundles that can be used across multiple applications and environments:
|
- **@serve.zone/cloudly** - Main orchestration platform
|
||||||
|
- **@serve.zone/api** - TypeScript/JavaScript API client
|
||||||
|
- **@serve.zone/cli** - Command-line interface
|
||||||
|
- **@serve.zone/interfaces** - Shared TypeScript interfaces
|
||||||
|
|
||||||
```typescript
|
## 🔒 Security Features
|
||||||
import { Cloudly } from '@serve.zone/cloudly';
|
|
||||||
|
|
||||||
async function createSecrets() {
|
- **End-to-end encryption** for secrets
|
||||||
const myCloudlyConfig = {
|
- **Role-based access control** (RBAC)
|
||||||
cfToken: 'your_cloudflare_api_token',
|
- **Automatic SSL/TLS** certificate management
|
||||||
environment: 'development',
|
- **Secure token-based authentication**
|
||||||
letsEncryptEmail: 'lets_encrypt_email@example.com',
|
- **Audit logging** for compliance
|
||||||
publicUrl: 'example.com',
|
|
||||||
publicPort: 8443,
|
|
||||||
hetznerToken: 'your_hetzner_api_token',
|
|
||||||
mongoDescriptor: {
|
|
||||||
mongoDbUrl: 'mongodb+srv://<username>:<password>@<cluster>.mongodb.net/myFirstDatabase',
|
|
||||||
mongoDbName: 'myDatabase',
|
|
||||||
mongoDbUser: 'myUser',
|
|
||||||
mongoDbPass: 'myPassword',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const myCloudlyInstance = new Cloudly(myCloudlyConfig);
|
## 🚢 Production Ready
|
||||||
await myCloudlyInstance.start();
|
|
||||||
|
|
||||||
const newSecretGroup = await myCloudlyInstance.secretManager.createSecretGroup({
|
Cloudly is battle-tested in production environments managing:
|
||||||
name: 'example_secret_group',
|
- High-traffic web applications
|
||||||
secrets: [
|
- Microservice architectures
|
||||||
{ key: 'SECRET_KEY', value: 's3cr3t' },
|
- CI/CD pipelines
|
||||||
],
|
- Data processing workloads
|
||||||
});
|
- Real-time communication systems
|
||||||
|
|
||||||
const newSecretBundle = await myCloudlyInstance.secretManager.createSecretBundle({
|
## 🤝 Development
|
||||||
name: 'example_bundle',
|
|
||||||
secretGroups: [newSecretGroup],
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Created Secret Group and Bundle:', newSecretGroup, newSecretBundle);
|
```bash
|
||||||
}
|
# Clone the repository
|
||||||
|
git clone https://gitlab.com/servezone/private/cloudly.git
|
||||||
|
|
||||||
createSecrets();
|
# Install dependencies
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
pnpm test
|
||||||
|
|
||||||
|
# Build the project
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# Start development mode
|
||||||
|
pnpm watch
|
||||||
```
|
```
|
||||||
|
|
||||||
Secrets, such as API keys and sensitive configuration data, are managed efficiently using secret groups and bundles. This structured approach to secret management enhances both security and accessibility.
|
## 📖 Documentation
|
||||||
|
|
||||||
### Task Scheduling and Management
|
For detailed documentation, API references, and guides, visit our [documentation site](https://cloudly.serve.zone).
|
||||||
|
|
||||||
With task buffers, you can schedule and manage background tasks integral to cloud operations:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Cloudly } from '@serve.zone/cloudly';
|
|
||||||
import { TaskBuffer } from '@push.rocks/taskbuffer';
|
|
||||||
|
|
||||||
async function scheduleTasks() {
|
|
||||||
const myCloudlyConfig = {
|
|
||||||
cfToken: 'your_cloudflare_api_token',
|
|
||||||
environment: 'development',
|
|
||||||
letsEncryptEmail: 'lets_encrypt_email@example.com',
|
|
||||||
publicUrl: 'example.com',
|
|
||||||
publicPort: 8443,
|
|
||||||
hetznerToken: 'your_hetzner_api_token',
|
|
||||||
mongoDescriptor: {
|
|
||||||
mongoDbUrl: 'mongodb+srv://<username>:<password>@<cluster>.mongodb.net/myFirstDatabase',
|
|
||||||
mongoDbName: 'myDatabase',
|
|
||||||
mongoDbUser: 'myUser',
|
|
||||||
mongoDbPass: 'myPassword',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const myCloudlyInstance = new Cloudly(myCloudlyConfig);
|
|
||||||
await myCloudlyInstance.start();
|
|
||||||
|
|
||||||
const taskManager = new TaskBuffer();
|
|
||||||
taskManager.scheduleEvery('minute', async () => {
|
|
||||||
console.log('Running scheduled task...');
|
|
||||||
// Task logic
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Tasks scheduled.');
|
|
||||||
}
|
|
||||||
|
|
||||||
scheduleTasks();
|
|
||||||
```
|
|
||||||
|
|
||||||
The example demonstrates setting up periodic task execution using task buffers as part of Cloudly's task management. Whether it's maintenance routines, data updates, or resource checks, tasks can be managed effectively.
|
|
||||||
|
|
||||||
This comprehensive overview of `@serve.zone/cloudly` is designed to help you leverage its full capabilities in managing multi-cloud environments. Each example is meant to serve as a starting point, and you are encouraged to explore further by consulting the relevant sections in the documentation, engaging with community discussions, or experimenting in your own environment.
|
|
||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
|
@@ -5,13 +5,13 @@ import * as cloudly from '../../ts/index.js';
|
|||||||
|
|
||||||
const stopFunctions: Array<() => Promise<void>> = [];
|
const stopFunctions: Array<() => Promise<void>> = [];
|
||||||
|
|
||||||
const tapToolsNodeMod = await import('@push.rocks/tapbundle/node');
|
const tapToolsNodeMod = await import('@git.zone/tstest/tapbundle_node');
|
||||||
const smartmongo = await tapToolsNodeMod.tapNodeTools.createSmartmongo();
|
const smartmongo = await tapToolsNodeMod.tapNodeTools.createSmartmongo();
|
||||||
stopFunctions.push(async () => {
|
stopFunctions.push(async () => {
|
||||||
await smartmongo.stopAndDumpToDir('./.nogit/mongodump');
|
await smartmongo.stopAndDumpToDir('./.nogit/mongodump');
|
||||||
});
|
});
|
||||||
const smarts3 = await tapToolsNodeMod.tapNodeTools.createSmarts3();
|
const smarts3 = await tapToolsNodeMod.tapNodeTools.createSmarts3();
|
||||||
await smarts3.createBucket('cloudly-test');
|
await smarts3.createBucket('cloudly_test_bucket');
|
||||||
stopFunctions.push(async () => {
|
stopFunctions.push(async () => {
|
||||||
await smarts3.stop();
|
await smarts3.stop();
|
||||||
});
|
});
|
||||||
@@ -23,7 +23,9 @@ export const testCloudlyConfig: cloudly.ICloudlyConfig = {
|
|||||||
publicUrl: '127.0.0.1',
|
publicUrl: '127.0.0.1',
|
||||||
publicPort: '8080',
|
publicPort: '8080',
|
||||||
mongoDescriptor: await smartmongo.getMongoDescriptor(),
|
mongoDescriptor: await smartmongo.getMongoDescriptor(),
|
||||||
s3Descriptor: await smarts3.getS3Descriptor(),
|
s3Descriptor: await smarts3.getS3Descriptor({
|
||||||
|
bucketName: 'cloudly_test_bucket'
|
||||||
|
}),
|
||||||
sslMode: 'none',
|
sslMode: 'none',
|
||||||
...(() => {
|
...(() => {
|
||||||
if (process.env.NPMCI_SECRET01) {
|
if (process.env.NPMCI_SECRET01) {
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { tap, expect } from '@push.rocks/tapbundle';
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
import * as helpers from './helpers/index.js';
|
import * as helpers from './helpers/index.js';
|
||||||
|
|
||||||
import * as cloudly from '../ts/index.js';
|
import * as cloudly from '../ts/index.js';
|
||||||
@@ -14,8 +14,10 @@ tap.preTask('should start cloudly', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
tap.preTask('should create a new machine user for testing', async () => {
|
tap.preTask('should create a new machine user for testing', async () => {
|
||||||
|
console.log('🔵 PreTask: Creating first machine user...');
|
||||||
const machineUser = new testCloudly.authManager.CUser();
|
const machineUser = new testCloudly.authManager.CUser();
|
||||||
machineUser.id = await testCloudly.authManager.CUser.getNewId();
|
machineUser.id = await testCloudly.authManager.CUser.getNewId();
|
||||||
|
console.log(` - User ID: ${machineUser.id}`);
|
||||||
machineUser.data = {
|
machineUser.data = {
|
||||||
type: 'machine',
|
type: 'machine',
|
||||||
username: 'test',
|
username: 'test',
|
||||||
@@ -27,48 +29,103 @@ tap.preTask('should create a new machine user for testing', async () => {
|
|||||||
}],
|
}],
|
||||||
role: 'admin',
|
role: 'admin',
|
||||||
};
|
};
|
||||||
|
console.log(` - Username: ${machineUser.data.username}`);
|
||||||
|
console.log(` - Role: ${machineUser.data.role}`);
|
||||||
|
console.log(` - Token: 'test'`);
|
||||||
|
console.log(` - Token roles: ${machineUser.data.tokens[0].assignedRoles}`);
|
||||||
await machineUser.save();
|
await machineUser.save();
|
||||||
|
console.log('✅ PreTask: First machine user saved successfully');
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should create a new cloudlyApiClient', async () => {
|
tap.test('should create a new cloudlyApiClient', async () => {
|
||||||
|
console.log('🔵 Test: Creating CloudlyApiClient...');
|
||||||
testClient = new cloudlyApiClient.CloudlyApiClient({
|
testClient = new cloudlyApiClient.CloudlyApiClient({
|
||||||
registerAs: 'api',
|
registerAs: 'api',
|
||||||
cloudlyUrl: `http://${helpers.testCloudlyConfig.publicUrl}:${helpers.testCloudlyConfig.publicPort}`,
|
cloudlyUrl: `http://${helpers.testCloudlyConfig.publicUrl}:${helpers.testCloudlyConfig.publicPort}`,
|
||||||
});
|
});
|
||||||
|
console.log(` - URL: http://${helpers.testCloudlyConfig.publicUrl}:${helpers.testCloudlyConfig.publicPort}`);
|
||||||
await testClient.start();
|
await testClient.start();
|
||||||
|
console.log('✅ CloudlyApiClient started successfully');
|
||||||
expect(testClient).toBeTruthy();
|
expect(testClient).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('create a new machine user', async () => {
|
tap.test('DEBUG: Check existing users', async () => {
|
||||||
const machineUser = await testCloudly.authManager.CUser.createMachineUser('test', 'api');
|
console.log('🔍 DEBUG: Checking existing users in database...');
|
||||||
machineUser.data.tokens.push({
|
const allUsers = await testCloudly.authManager.CUser.getInstances({});
|
||||||
token: 'test',
|
console.log(` - Total users found: ${allUsers.length}`);
|
||||||
expiresAt: Date.now() + 3600 * 1000 * 24 * 365,
|
for (const user of allUsers) {
|
||||||
assignedRoles: ['api'],
|
console.log(` - User: ${user.data.username} (ID: ${user.id})`);
|
||||||
})
|
console.log(` - Type: ${user.data.type}`);
|
||||||
await machineUser.save();
|
console.log(` - Role: ${user.data.role}`);
|
||||||
})
|
console.log(` - Tokens: ${user.data.tokens.length}`);
|
||||||
|
for (const token of user.data.tokens) {
|
||||||
|
console.log(` - Token: '${token.token}' | Roles: ${token.assignedRoles?.join(', ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
tap.test('should get an identity', async () => {
|
tap.test('should get an identity', async () => {
|
||||||
|
console.log('🔵 Test: Getting identity by token...');
|
||||||
|
console.log(` - Using token: 'test'`);
|
||||||
|
console.log(` - API URL: http://${helpers.testCloudlyConfig.publicUrl}:${helpers.testCloudlyConfig.publicPort}`);
|
||||||
|
|
||||||
|
try {
|
||||||
const identity = await testClient.getIdentityByToken('test');
|
const identity = await testClient.getIdentityByToken('test');
|
||||||
|
console.log('✅ Identity retrieved successfully:');
|
||||||
|
console.log(` - Identity exists: ${!!identity}`);
|
||||||
|
if (identity) {
|
||||||
|
console.log(` - Identity data:`, JSON.stringify(identity, null, 2));
|
||||||
|
}
|
||||||
expect(identity).toBeTruthy();
|
expect(identity).toBeTruthy();
|
||||||
console.log(identity);
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to get identity:');
|
||||||
|
console.error(` - Error message: ${error.message}`);
|
||||||
|
console.error(` - Error stack:`, error.stack);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let image: Image;
|
let image: Image;
|
||||||
tap.test('should create and upload an image', async () => {
|
tap.test('should create and upload an image', async () => {
|
||||||
|
console.log('🔵 Test: Creating and uploading image...');
|
||||||
|
console.log(` - Image name: 'test'`);
|
||||||
|
console.log(` - Image description: 'test'`);
|
||||||
|
|
||||||
|
try {
|
||||||
image = await testClient.image.createImage({
|
image = await testClient.image.createImage({
|
||||||
name: 'test',
|
name: 'test',
|
||||||
description: 'test'
|
description: 'test'
|
||||||
});
|
});
|
||||||
console.log('created image: ', image);
|
console.log('✅ Image created successfully:');
|
||||||
|
console.log(` - Image ID: ${image?.id}`);
|
||||||
|
console.log(` - Image data:`, image);
|
||||||
expect(image).toBeInstanceOf(Image);
|
expect(image).toBeInstanceOf(Image);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to create image:');
|
||||||
|
console.error(` - Error message: ${error.message}`);
|
||||||
|
console.error(` - Error stack:`, error.stack);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
tap.test('should upload an image version', async () => {
|
tap.test('should upload an image version', async () => {
|
||||||
|
console.log('🔵 Test: Uploading image version...');
|
||||||
|
console.log(` - Version: 'v1.0.0'`);
|
||||||
|
console.log(` - Image exists: ${!!image}`);
|
||||||
|
console.log(` - Image ID: ${image?.id}`);
|
||||||
|
|
||||||
|
try {
|
||||||
const imageStream = await helpers.getAlpineImageReadableStream();
|
const imageStream = await helpers.getAlpineImageReadableStream();
|
||||||
|
console.log(' - Image stream obtained successfully');
|
||||||
|
|
||||||
await image.pushImageVersion('v1.0.0', imageStream);
|
await image.pushImageVersion('v1.0.0', imageStream);
|
||||||
|
console.log('✅ Image version uploaded successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to upload image version:');
|
||||||
|
console.error(` - Error message: ${error.message}`);
|
||||||
|
console.error(` - Error stack:`, error.stack);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should stop the apiclient', async (toolsArg) => {
|
tap.test('should stop the apiclient', async (toolsArg) => {
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import * as helpers from './helpers/index.js';
|
import * as helpers from './helpers/index.js';
|
||||||
|
|
||||||
import * as cloudly from '../ts/index.js';
|
import * as cloudly from '../ts/index.js';
|
||||||
|
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/cloudly',
|
name: '@serve.zone/cloudly',
|
||||||
version: '4.11.0',
|
version: '5.2.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.'
|
||||||
}
|
}
|
||||||
|
@@ -63,7 +63,7 @@ for (let i = 0; i < demoSecretGroups.length; i++) {
|
|||||||
id: `configBundleId${i + 1}`,
|
id: `configBundleId${i + 1}`,
|
||||||
data: {
|
data: {
|
||||||
name: `Demo Config Bundle ${i + 1}`,
|
name: `Demo Config Bundle ${i + 1}`,
|
||||||
includedImages: [],
|
imageClaims: [],
|
||||||
type: 'external',
|
type: 'external',
|
||||||
description: 'Demo Purpose',
|
description: 'Demo Purpose',
|
||||||
includedSecretGroupIds: [secretGroup.id],
|
includedSecretGroupIds: [secretGroup.id],
|
||||||
|
@@ -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,33 +20,30 @@ 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',
|
||||||
mongoDescriptor: {
|
mongoDescriptor: {
|
||||||
mongoDbUrl: 'MONGODB_URL',
|
mongoDbUrl: 'MONGODB_URL',
|
||||||
mongoDbName: 'MONGODB_DATABASE',
|
mongoDbName: 'MONGODB_NAME',
|
||||||
mongoDbUser: 'MONGODB_USER',
|
mongoDbUser: 'MONGODB_USER',
|
||||||
mongoDbPass: 'MONGODB_PASSWORD',
|
mongoDbPass: 'MONGODB_PASS',
|
||||||
},
|
},
|
||||||
s3Descriptor: {
|
s3Descriptor: {
|
||||||
endpoint: 'S3_ENDPOINT',
|
endpoint: 'S3_ENDPOINT',
|
||||||
accessKey: 'S3_ACCESSKEY',
|
accessKey: 'S3_ACCESSKEY',
|
||||||
accessSecret: 'S3_SECRETKEY',
|
accessSecret: 'S3_SECRETKEY',
|
||||||
port: 'S3_PORT', // Note: This will remain as a string. Ensure to parse it to an integer where it's used.
|
port: 'S3_PORT', // Note: This will remain as a string. Ensure to parse it to an integer where it's used.
|
||||||
useSsl: true,
|
useSsl: 'boolean:S3_USESSL' as any as boolean,
|
||||||
|
bucketName: 'S3_BUCKET'
|
||||||
},
|
},
|
||||||
sslMode:
|
sslMode:
|
||||||
'SERVEZONE_SSLMODE' as plugins.servezoneInterfaces.data.ICloudlyConfig['sslMode'],
|
'SERVEZONE_SSLMODE' as plugins.servezoneInterfaces.data.ICloudlyConfig['sslMode'],
|
||||||
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(),
|
||||||
};
|
};
|
||||||
|
@@ -16,24 +16,23 @@ export class CloudlyCoreflowManager {
|
|||||||
|
|
||||||
this.typedRouter.addTypedHandler<plugins.servezoneInterfaces.requests.identity.IRequest_Any_Cloudly_CoreflowManager_GetIdentityByToken>(
|
this.typedRouter.addTypedHandler<plugins.servezoneInterfaces.requests.identity.IRequest_Any_Cloudly_CoreflowManager_GetIdentityByToken>(
|
||||||
new plugins.typedrequest.TypedHandler('getIdentityByToken', async (requestData) => {
|
new plugins.typedrequest.TypedHandler('getIdentityByToken', async (requestData) => {
|
||||||
|
// Use getInstance with $elemMatch for querying nested arrays
|
||||||
const user = await this.cloudlyRef.authManager.CUser.getInstance({
|
const user = await this.cloudlyRef.authManager.CUser.getInstance({
|
||||||
data: {
|
data: {
|
||||||
tokens: [
|
tokens: {
|
||||||
{
|
$elemMatch: { token: requestData.token },
|
||||||
token: requestData.token,
|
},
|
||||||
},
|
},
|
||||||
], // find the proper user here.
|
|
||||||
} as any,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new plugins.typedrequest.TypedResponseError(
|
throw new plugins.typedrequest.TypedResponseError(
|
||||||
'The supplied token is not valid. No matching user found.',
|
'The supplied token is not valid. No matching user found.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (user.data.type !== 'machine') {
|
if (user.data.type !== 'machine') {
|
||||||
throw new plugins.typedrequest.TypedResponseError(
|
throw new plugins.typedrequest.TypedResponseError(
|
||||||
'The supplied token is not valid. The user is not a machine.',
|
'The supplied token is not valid. The user is not a machine.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let cluster: Cluster;
|
let cluster: Cluster;
|
||||||
@@ -61,7 +60,7 @@ export class CloudlyCoreflowManager {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// lets enable the getting of cluster configs
|
// lets enable the getting of cluster configs
|
||||||
@@ -76,10 +75,10 @@ export class CloudlyCoreflowManager {
|
|||||||
console.log('got cluster config and sending it back to coreflow');
|
console.log('got cluster config and sending it back to coreflow');
|
||||||
return {
|
return {
|
||||||
configData: await cluster.createSavableObject(),
|
configData: await cluster.createSavableObject(),
|
||||||
deploymentDirectives: [],
|
services: [],
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// lets enable getting of certificates
|
// lets enable getting of certificates
|
||||||
@@ -89,14 +88,14 @@ export class CloudlyCoreflowManager {
|
|||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
console.log(`incoming API request for certificate ${dataArg.domainName}`);
|
console.log(`incoming API request for certificate ${dataArg.domainName}`);
|
||||||
const cert = await this.cloudlyRef.letsencryptConnector.getCertificateForDomain(
|
const cert = await this.cloudlyRef.letsencryptConnector.getCertificateForDomain(
|
||||||
dataArg.domainName,
|
dataArg.domainName
|
||||||
);
|
);
|
||||||
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,
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -156,7 +156,7 @@ export class ImageManager {
|
|||||||
this.smartbucketInstance = new plugins.smartbucket.SmartBucket(
|
this.smartbucketInstance = new plugins.smartbucket.SmartBucket(
|
||||||
this.cloudlyRef.config.data.s3Descriptor,
|
this.cloudlyRef.config.data.s3Descriptor,
|
||||||
);
|
);
|
||||||
const bucket = await this.smartbucketInstance.getBucketByName('cloudly-test');
|
const bucket = await this.smartbucketInstance.getBucketByName(s3Descriptor.bucketName);
|
||||||
await bucket.fastPut({ path: 'images/00init', contents: 'init' });
|
await bucket.fastPut({ path: 'images/00init', contents: 'init' });
|
||||||
|
|
||||||
this.imageDir = await bucket.getDirectoryFromPath({
|
this.imageDir = await bucket.getDirectoryFromPath({
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@@ -59,4 +59,16 @@ export class SecretBundle extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
}
|
}
|
||||||
return returnObject;
|
return returnObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getFlatKeyValueObject(environmentArg: string) {
|
||||||
|
if (!environmentArg) {
|
||||||
|
throw new Error('environment is required');
|
||||||
|
}
|
||||||
|
const secretGroups = await this.getSecretGroups();
|
||||||
|
const returnObject = {};
|
||||||
|
for (const secretGroup of secretGroups) {
|
||||||
|
returnObject[secretGroup.data.key] = secretGroup.data.environments[environmentArg].value;
|
||||||
|
}
|
||||||
|
return returnObject;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,3 +1,4 @@
|
|||||||
|
import { SecretBundle } from 'ts/manager.secret/classes.secretbundle.js';
|
||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import { ServiceManager } from './classes.servicemanager.js';
|
import { ServiceManager } from './classes.servicemanager.js';
|
||||||
|
|
||||||
@@ -6,9 +7,51 @@ export class Service extends plugins.smartdata.SmartDataDbDoc<
|
|||||||
plugins.servezoneInterfaces.data.IService,
|
plugins.servezoneInterfaces.data.IService,
|
||||||
ServiceManager
|
ServiceManager
|
||||||
> {
|
> {
|
||||||
|
// STATIC
|
||||||
|
public static async getServiceById(serviceIdArg: string) {
|
||||||
|
const service = await this.getInstance({
|
||||||
|
id: serviceIdArg,
|
||||||
|
});
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getServices() {
|
||||||
|
const services = await this.getInstances({});
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async createService(serviceDataArg: Partial<plugins.servezoneInterfaces.data.IService['data']>) {
|
||||||
|
const service = new Service();
|
||||||
|
service.id = await Service.getNewId();
|
||||||
|
Object.assign(service, serviceDataArg);
|
||||||
|
await service.save();
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
|
// INSTANCE
|
||||||
@plugins.smartdata.svDb()
|
@plugins.smartdata.svDb()
|
||||||
public id: string;
|
public id: string;
|
||||||
|
|
||||||
@plugins.smartdata.svDb()
|
@plugins.smartdata.svDb()
|
||||||
public data: plugins.servezoneInterfaces.data.IService['data'];
|
public data: plugins.servezoneInterfaces.data.IService['data'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a service runs in a specific environment
|
||||||
|
* so -> this method returns the secret bundles as a flat object accordingly.
|
||||||
|
* in other words, it resolves secret groups for the relevant environment
|
||||||
|
* @param environmentArg
|
||||||
|
*/
|
||||||
|
public async getSecretBundlesAsFlatObject(environmentArg: string = 'production') {
|
||||||
|
const secreBundleIds = this.data.additionalSecretBundleIds || [];
|
||||||
|
secreBundleIds.push(this.data.secretBundleId); // put this last, so it overwrites any other secret bundles.
|
||||||
|
let finalFlatObject = {};
|
||||||
|
for (const secretBundleId of secreBundleIds) {
|
||||||
|
const secretBundle = await SecretBundle.getInstance({
|
||||||
|
id: secretBundleId,
|
||||||
|
});
|
||||||
|
const flatObject = await secretBundle.getFlatKeyValueObject(environmentArg);
|
||||||
|
Object.assign(finalFlatObject, flatObject);
|
||||||
|
}
|
||||||
|
return finalFlatObject;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -35,5 +35,66 @@ export class ServiceManager {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_GetServiceSecretBundlesAsFlatObject>(
|
||||||
|
'getServiceSecretBundlesAsFlatObject',
|
||||||
|
async (dataArg) => {
|
||||||
|
const service = await Service.getInstance({
|
||||||
|
id: dataArg.serviceId,
|
||||||
|
});
|
||||||
|
const flatKeyValueObject = await service.getSecretBundlesAsFlatObject(dataArg.environment);
|
||||||
|
return {
|
||||||
|
flatKeyValueObject: flatKeyValueObject,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_CreateService>(
|
||||||
|
'createService',
|
||||||
|
async (dataArg) => {
|
||||||
|
const service = await Service.createService(dataArg.serviceData);
|
||||||
|
return {
|
||||||
|
service: await service.createSavableObject(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_UpdateService>(
|
||||||
|
'updateService',
|
||||||
|
async (dataArg) => {
|
||||||
|
const service = await Service.getInstance({
|
||||||
|
id: dataArg.serviceId,
|
||||||
|
});
|
||||||
|
service.data = {
|
||||||
|
...service.data,
|
||||||
|
...dataArg.serviceData,
|
||||||
|
};
|
||||||
|
await service.save();
|
||||||
|
return {
|
||||||
|
service: await service.createSavableObject(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_DeleteServiceById>(
|
||||||
|
'deleteServiceById',
|
||||||
|
async (dataArg) => {
|
||||||
|
const service = await Service.getInstance({
|
||||||
|
id: dataArg.serviceId,
|
||||||
|
});
|
||||||
|
await service.delete();
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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';
|
@@ -59,7 +59,7 @@ export class SecretBundle implements plugins.servezoneInterfaces.data.ISecretBun
|
|||||||
description: secretBundleDataArg.description,
|
description: secretBundleDataArg.description,
|
||||||
type: secretBundleDataArg.type,
|
type: secretBundleDataArg.type,
|
||||||
authorizations: secretBundleDataArg.authorizations,
|
authorizations: secretBundleDataArg.authorizations,
|
||||||
includedImages: secretBundleDataArg.includedImages,
|
imageClaims: secretBundleDataArg.imageClaims,
|
||||||
includedSecretGroupIds: secretBundleDataArg.includedSecretGroupIds,
|
includedSecretGroupIds: secretBundleDataArg.includedSecretGroupIds,
|
||||||
includedTags: secretBundleDataArg.includedTags,
|
includedTags: secretBundleDataArg.includedTags,
|
||||||
},
|
},
|
||||||
|
@@ -40,19 +40,7 @@ export class Service implements plugins.servezoneInterfaces.data.IService {
|
|||||||
);
|
);
|
||||||
const response = await createServiceTR.fire({
|
const response = await createServiceTR.fire({
|
||||||
identity: cloudlyClientRef.identity,
|
identity: cloudlyClientRef.identity,
|
||||||
name: serviceDataArg.name,
|
serviceData: serviceDataArg as plugins.servezoneInterfaces.data.IService['data'],
|
||||||
description: serviceDataArg.description,
|
|
||||||
imageId: serviceDataArg.imageId,
|
|
||||||
imageVersion: serviceDataArg.imageVersion,
|
|
||||||
environment: {},
|
|
||||||
secretBundleId: null,
|
|
||||||
scaleFactor: 1,
|
|
||||||
balancingStrategy: serviceDataArg.balancingStrategy,
|
|
||||||
ports: {
|
|
||||||
web: null,
|
|
||||||
},
|
|
||||||
resources: serviceDataArg.resources,
|
|
||||||
domains: [],
|
|
||||||
});
|
});
|
||||||
const newService = new Service(cloudlyClientRef);
|
const newService = new Service(cloudlyClientRef);
|
||||||
Object.assign(newService, response.service);
|
Object.assign(newService, response.service);
|
||||||
@@ -75,6 +63,16 @@ export class Service implements plugins.servezoneInterfaces.data.IService {
|
|||||||
* In other words, it resolves secret groups and
|
* In other words, it resolves secret groups and
|
||||||
*/
|
*/
|
||||||
public async getSecretBundleAsFlatObject(environmentArg: string = 'production') {
|
public async getSecretBundleAsFlatObject(environmentArg: string = 'production') {
|
||||||
|
const getServiceSecretBundlesAsFlatObjectTR = this.cloudlyClientRef.typedsocketClient.createTypedRequest<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_GetServiceSecretBundlesAsFlatObject>(
|
||||||
|
'getServiceSecretBundlesAsFlatObject'
|
||||||
|
);
|
||||||
|
const response = await getServiceSecretBundlesAsFlatObjectTR.fire({
|
||||||
|
identity: this.cloudlyClientRef.identity,
|
||||||
|
serviceId: this.id,
|
||||||
|
environment: environmentArg,
|
||||||
|
});
|
||||||
|
const flatKeyValueObject: {[key: string]: string} = response.flatKeyValueObject;
|
||||||
|
|
||||||
|
return flatKeyValueObject;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,186 +1,290 @@
|
|||||||
# @serve.zone/api
|
# @serve.zone/api 🔌
|
||||||
|
|
||||||
The `@serve.zone/api` module is a robust and versatile API client, designed to facilitate seamless communication with various cloud resources managed by the Cloudly platform. This API client extends a rich set of functionalities, offering developers a comprehensive and programmable interface for interacting with their multi-cloud infrastructure.
|
**The powerful API client for Cloudly.** Connect your applications to multi-cloud infrastructure with type-safe, real-time communication.
|
||||||
|
|
||||||
## Install
|
## 🎯 What is @serve.zone/api?
|
||||||
|
|
||||||
To install the `@serve.zone/api` package, execute the following command in your terminal:
|
This is your programmatic gateway to the Cloudly platform. Built with TypeScript, it provides a robust, type-safe interface for managing cloud resources, orchestrating containers, and automating infrastructure operations.
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- **🔒 Type-Safe** - Full TypeScript support with comprehensive interfaces
|
||||||
|
- **⚡ Real-Time** - WebSocket-based communication for instant updates
|
||||||
|
- **🔑 Secure Authentication** - Token-based identity management
|
||||||
|
- **📦 Resource Management** - Complete control over clusters, images, and services
|
||||||
|
- **🎭 Multi-Identity** - Support for service accounts and user authentication
|
||||||
|
- **🔄 Reactive Streams** - RxJS integration for event-driven programming
|
||||||
|
|
||||||
|
## 🚀 Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install @serve.zone/api --save
|
pnpm add @serve.zone/api
|
||||||
```
|
```
|
||||||
|
|
||||||
This command will download the module and add it to your project's `package.json` dependencies, allowing you to utilize its capabilities within your application.
|
## 🎬 Quick Start
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
The `@serve.zone/api` client is tailored to handle various operations within a multi-cloud environment efficiently. Throughout this section, we will explore the different features and use-cases of this API client, aiding you in leveraging its full potential.
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
Before integrating `@serve.zone/api` into your project, ensure the following prerequisites are satisfied:
|
|
||||||
- You have Node.js installed on your system (preferably the latest Long-Term Support version).
|
|
||||||
- You're utilizing a TypeScript-compatible environment for development.
|
|
||||||
|
|
||||||
### Establishing an API Client Instance
|
|
||||||
|
|
||||||
The cornerstone of using `@serve.zone/api` is initializing a `CloudlyApiClient` instance. It serves as the main point of interaction, enabling communication with underlying cloud infrastructures managed by Cloudly. Here's a basic setup guide:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { CloudlyApiClient, TClientType } from '@serve.zone/api';
|
|
||||||
|
|
||||||
async function initializeClient() {
|
|
||||||
const client = new CloudlyApiClient({
|
|
||||||
registerAs: 'api' as TClientType,
|
|
||||||
cloudlyUrl: 'https://yourcloudly.url:443'
|
|
||||||
});
|
|
||||||
|
|
||||||
await client.start();
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cloudlyClient = await initializeClient();
|
|
||||||
```
|
|
||||||
|
|
||||||
The above code initializes the `CloudlyApiClient` object, connecting your application to the configured Cloudly environment.
|
|
||||||
|
|
||||||
### Authentication and Identity Management
|
|
||||||
|
|
||||||
To execute operations via the API client, authenticated access is necessary. The most prevalent method for this is obtaining an identity token using a service token:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { CloudlyApiClient } from '@serve.zone/api';
|
import { CloudlyApiClient } from '@serve.zone/api';
|
||||||
|
|
||||||
async function authenticate(client: CloudlyApiClient, serviceToken: string) {
|
// Initialize the client
|
||||||
const identity = await client.getIdentityByToken(serviceToken, {
|
const client = new CloudlyApiClient({
|
||||||
|
registerAs: 'api',
|
||||||
|
cloudlyUrl: 'https://cloudly.example.com:443'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start the connection
|
||||||
|
await client.start();
|
||||||
|
|
||||||
|
// Authenticate with a service token
|
||||||
|
const identity = await client.getIdentityByToken('your-service-token', {
|
||||||
tagConnection: true,
|
tagConnection: true,
|
||||||
statefullIdentity: true
|
statefullIdentity: true
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Authenticated identity:`, identity);
|
console.log('🎉 Connected as:', identity.name);
|
||||||
return identity;
|
|
||||||
}
|
|
||||||
|
|
||||||
const serviceToken = 'your_service_token';
|
|
||||||
const identity = await authenticate(cloudlyClient, serviceToken);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
In this function, the `getIdentityByToken` method authenticates using a service token and acquires an identity object that includes user details and security claims.
|
## 🔐 Authentication
|
||||||
|
|
||||||
### Interacting with Cloudly Features
|
### Service Token Authentication
|
||||||
|
|
||||||
#### Image Management
|
|
||||||
|
|
||||||
Image management is one of the key features supported by the API Client. You can create, upload, and manage Docker images easily within your cloud ecosystem:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
async function manageImages(client: CloudlyApiClient, identity) {
|
// Authenticate using a service token
|
||||||
// Creating a new image
|
const identity = await client.getIdentityByToken(serviceToken, {
|
||||||
const newImage = await client.images.createImage({
|
tagConnection: true, // Tag this connection with the identity
|
||||||
name: 'my_new_image',
|
statefullIdentity: true // Maintain state across reconnections
|
||||||
description: 'A test image'
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Identity Management
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Get current identity
|
||||||
|
const currentIdentity = client.identity;
|
||||||
|
|
||||||
|
// Check permissions
|
||||||
|
if (currentIdentity.permissions.includes('cluster:write')) {
|
||||||
|
// Perform cluster operations
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 Core Operations
|
||||||
|
|
||||||
|
### 🐳 Image Management
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Create an image entry
|
||||||
|
const image = await client.images.createImage({
|
||||||
|
name: 'my-app',
|
||||||
|
description: 'Production application image'
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Created image:`, newImage);
|
// Push a new version
|
||||||
|
const imageStream = fs.createReadStream('app.tar');
|
||||||
|
await image.pushImageVersion('2.0.0', imageStream);
|
||||||
|
|
||||||
// Uploading an image version
|
// List all images
|
||||||
const imageStream = fetchYourImageStreamHere(); // Provide the source image stream
|
const images = await client.images.listImages();
|
||||||
await newImage.pushImageVersion('1.0.0', imageStream);
|
|
||||||
console.log('Image version uploaded successfully.');
|
|
||||||
}
|
|
||||||
|
|
||||||
await manageImages(cloudlyClient, identity);
|
|
||||||
|
|
||||||
// Helper function for obtaining image stream (implement accordingly)
|
|
||||||
function fetchYourImageStreamHere() {
|
|
||||||
// Logic to fetch and return a readable stream for your image
|
|
||||||
return new ReadableStream<Uint8Array>();
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
In this example, the `manageImages` function underscores the typical workflow of creating an image entry within Cloudly and then proceeding to upload a specific version using the `pushImageVersion` method.
|
### 🌐 Cluster Operations
|
||||||
|
|
||||||
#### Cluster Configuration
|
|
||||||
|
|
||||||
Another powerful capability is managing clusters, which allows for orchestrating and configuring Docker Swarm clusters:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
async function configureCluster(client: CloudlyApiClient, identity) {
|
// Get cluster configuration
|
||||||
// Fetching cluster configuration
|
|
||||||
const clusterConfig = await client.getClusterConfigFromCloudlyByIdentity(identity);
|
const clusterConfig = await client.getClusterConfigFromCloudlyByIdentity(identity);
|
||||||
console.log(`Cluster configuration retrieved:`, clusterConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
await configureCluster(cloudlyClient, identity);
|
// Deploy to cluster
|
||||||
|
await client.deployToCluster({
|
||||||
|
clusterName: 'production',
|
||||||
|
serviceName: 'api-service',
|
||||||
|
image: 'my-app:2.0.0',
|
||||||
|
replicas: 3
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
The `getClusterConfigFromCloudlyByIdentity` method retrieved the configuration needed to set up and manage your clusters within the multi-cloud environment.
|
### 🔒 Certificate Management
|
||||||
|
|
||||||
### Advanced Communication via Typed Sockets
|
|
||||||
|
|
||||||
The API client leverages `TypedRequest` and `TypedSocket` from the `@api.global` family, enabling statically-typed, real-time communication. Here's an example demonstrating socket integration:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
async function configureSocketCommunication(client: CloudlyApiClient) {
|
// Request SSL certificate
|
||||||
client.configUpdateSubject.subscribe({
|
|
||||||
next: (configData) => {
|
|
||||||
console.log('Received configuration update:', configData);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
client.serverActionSubject.subscribe({
|
|
||||||
next: (actionRequest) => {
|
|
||||||
console.log('Server action requested:', actionRequest);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
configureSocketCommunication(cloudlyClient);
|
|
||||||
```
|
|
||||||
|
|
||||||
The client utilizes RxJS `Subject` to enable simple yet powerful handling of incoming socket requests, whereby one can act upon updates and actions as they occur.
|
|
||||||
|
|
||||||
### Integrating Certificates
|
|
||||||
|
|
||||||
Certificate operations, such as obtaining SSL certificates for your domains, are also streamlined using this API client:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
async function retrieveCertificate(client: CloudlyApiClient, domainName: string, identity) {
|
|
||||||
const certificate = await client.getCertificateForDomain({
|
const certificate = await client.getCertificateForDomain({
|
||||||
domainName: domainName,
|
domainName: 'api.example.com',
|
||||||
type: 'ssl',
|
type: 'ssl',
|
||||||
identity: identity
|
identity: identity
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Retrieved SSL Certificate:', certificate);
|
// Use certificate in your application
|
||||||
}
|
console.log('Certificate:', certificate.cert);
|
||||||
|
console.log('Private Key:', certificate.key);
|
||||||
const yourDomain = 'example.com';
|
|
||||||
await retrieveCertificate(cloudlyClient, yourDomain, identity);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This example demonstrates fetching SSL certificates using given domain credentials and an authenticated identity.
|
### 🔐 Secret Management
|
||||||
|
|
||||||
### API Client Cleanup
|
|
||||||
|
|
||||||
When operations are complete and the application is shutting down, it's crucial to gracefully terminate the API client connection:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
async function cleanup(client: CloudlyApiClient) {
|
// Create secret group
|
||||||
await client.stop();
|
const secretGroup = await client.secrets.createSecretGroup({
|
||||||
console.log('Cloudly API client disconnected gracefully.');
|
name: 'api-secrets',
|
||||||
}
|
secrets: [
|
||||||
|
{ key: 'DATABASE_URL', value: 'postgres://...' },
|
||||||
|
{ key: 'REDIS_URL', value: 'redis://...' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
await cleanup(cloudlyClient);
|
// Retrieve secrets
|
||||||
|
const secrets = await client.secrets.getSecretGroup('api-secrets');
|
||||||
```
|
```
|
||||||
|
|
||||||
By invoking the `stop` method, the API client securely terminates its connection to ensure no resources are left hanging, preventing potential memory leaks.
|
## 🔄 Real-Time Updates
|
||||||
|
|
||||||
### Miscellaneous Features
|
Subscribe to configuration changes and server actions using RxJS:
|
||||||
|
|
||||||
This section would be remiss without mentioning various utility functionalities such as secret management, server actions, DNS configurator options, and more, all underpinned by an intelligently designed API, enriching cloud resource interactivity.
|
```typescript
|
||||||
|
// Listen for configuration updates
|
||||||
|
client.configUpdateSubject.subscribe({
|
||||||
|
next: (config) => {
|
||||||
|
console.log('📡 Configuration updated:', config);
|
||||||
|
// React to configuration changes
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
In conclusion, by employing `@serve.zone/api`, developers gain unparalleled access to a multitude of modular functions pertinent to multi-cloud administration, significantly amplifying productivity and management effectiveness across diverse computing environments.
|
// Handle server action requests
|
||||||
|
client.serverActionSubject.subscribe({
|
||||||
|
next: (action) => {
|
||||||
|
console.log('⚡ Server action:', action.type);
|
||||||
|
// Process server-initiated actions
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Advanced Usage
|
||||||
|
|
||||||
|
### Streaming Operations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Stream logs from a service
|
||||||
|
const logStream = await client.logs.streamLogs({
|
||||||
|
service: 'api-service',
|
||||||
|
follow: true
|
||||||
|
});
|
||||||
|
|
||||||
|
logStream.on('data', (log) => {
|
||||||
|
console.log(log.message);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Batch Operations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Deploy multiple services
|
||||||
|
const deployments = await Promise.all([
|
||||||
|
client.deploy({ service: 'frontend', image: 'app:latest' }),
|
||||||
|
client.deploy({ service: 'backend', image: 'api:latest' }),
|
||||||
|
client.deploy({ service: 'worker', image: 'worker:latest' })
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
try {
|
||||||
|
await client.start();
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'AUTH_FAILED') {
|
||||||
|
console.error('Authentication failed:', error.message);
|
||||||
|
} else if (error.code === 'CONNECTION_LOST') {
|
||||||
|
console.error('Connection lost, retrying...');
|
||||||
|
await client.reconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧹 Cleanup
|
||||||
|
|
||||||
|
Always gracefully disconnect when done:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Stop the client connection
|
||||||
|
await client.stop();
|
||||||
|
console.log('✅ Disconnected cleanly');
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔌 API Reference
|
||||||
|
|
||||||
|
### CloudlyApiClient
|
||||||
|
|
||||||
|
Main client class for interacting with Cloudly.
|
||||||
|
|
||||||
|
#### Constructor Options
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ICloudlyApiClientOptions {
|
||||||
|
registerAs: TClientType; // 'api' | 'cli' | 'web'
|
||||||
|
cloudlyUrl: string; // Full URL including protocol and port
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Methods
|
||||||
|
|
||||||
|
- `start()` - Initialize connection
|
||||||
|
- `stop()` - Close connection
|
||||||
|
- `getIdentityByToken()` - Authenticate with token
|
||||||
|
- `getClusterConfigFromCloudlyByIdentity()` - Get cluster configuration
|
||||||
|
- `getCertificateForDomain()` - Request SSL certificate
|
||||||
|
- `images` - Image management namespace
|
||||||
|
- `secrets` - Secret management namespace
|
||||||
|
- `clusters` - Cluster management namespace
|
||||||
|
|
||||||
|
## 🎬 Complete Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { CloudlyApiClient } from '@serve.zone/api';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// Initialize client
|
||||||
|
const client = new CloudlyApiClient({
|
||||||
|
registerAs: 'api',
|
||||||
|
cloudlyUrl: 'https://cloudly.example.com:443'
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Connect and authenticate
|
||||||
|
await client.start();
|
||||||
|
const identity = await client.getIdentityByToken(process.env.SERVICE_TOKEN, {
|
||||||
|
tagConnection: true,
|
||||||
|
statefullIdentity: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create and deploy an image
|
||||||
|
const image = await client.images.createImage({
|
||||||
|
name: 'my-service',
|
||||||
|
description: 'Microservice application'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Push image version
|
||||||
|
const stream = getImageStream(); // Your image stream
|
||||||
|
await image.pushImageVersion('1.0.0', stream);
|
||||||
|
|
||||||
|
// Deploy to cluster
|
||||||
|
await client.deployToCluster({
|
||||||
|
clusterName: 'production',
|
||||||
|
serviceName: 'my-service',
|
||||||
|
image: 'my-service:1.0.0',
|
||||||
|
replicas: 3,
|
||||||
|
environment: {
|
||||||
|
NODE_ENV: 'production'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Deployment successful!');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error:', error);
|
||||||
|
} finally {
|
||||||
|
await client.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
```
|
||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
|
15
ts_cliclient/classes.cliclient.ts
Normal file
15
ts_cliclient/classes.cliclient.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import * as plugins from './plugins.js';
|
||||||
|
import { CloudlyApiClient } from '@serve.zone/api';
|
||||||
|
|
||||||
|
export class CliClient {
|
||||||
|
public cloudlyApiClient: CloudlyApiClient;
|
||||||
|
|
||||||
|
constructor(cloudlyApiClientArg: CloudlyApiClient) {
|
||||||
|
this.cloudlyApiClient = cloudlyApiClientArg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getClusters() {
|
||||||
|
const clusters = await this.cloudlyApiClient.cluster.getClusters();
|
||||||
|
console.log(clusters);
|
||||||
|
}
|
||||||
|
}
|
@@ -1 +1,11 @@
|
|||||||
console.log('this is the cli client.');
|
import * as plugins from './plugins.js';
|
||||||
|
import { CliClient } from "./classes.cliclient.js";
|
||||||
|
|
||||||
|
export const runCli = async () => {
|
||||||
|
const cliQenv = new plugins.qenv.Qenv();
|
||||||
|
const apiClient = new plugins.servezoneApi.CloudlyApiClient({
|
||||||
|
registerAs: 'cli',
|
||||||
|
cloudlyUrl: await cliQenv.getEnvVarOnDemand('CLOUDLY_URL'),
|
||||||
|
});
|
||||||
|
const cliClient = new CliClient(apiClient);
|
||||||
|
};
|
17
ts_cliclient/plugins.ts
Normal file
17
ts_cliclient/plugins.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// @serve.zone scope
|
||||||
|
import * as servezoneApi from '@serve.zone/api';
|
||||||
|
import * as servezoneInterfaces from '@serve.zone/interfaces';
|
||||||
|
|
||||||
|
export {
|
||||||
|
servezoneApi,
|
||||||
|
servezoneInterfaces
|
||||||
|
}
|
||||||
|
|
||||||
|
// @push.rocks scope
|
||||||
|
import * as projectinfo from '@push.rocks/projectinfo';
|
||||||
|
import * as qenv from '@push.rocks/qenv';
|
||||||
|
|
||||||
|
export {
|
||||||
|
projectinfo,
|
||||||
|
qenv,
|
||||||
|
}
|
@@ -1,246 +1,342 @@
|
|||||||
# @serve.zone/cli
|
# @serve.zone/cli 🚀
|
||||||
|
|
||||||
A comprehensive command-line interface (CLI) tool for managing multi-cloud environments, leveraging the features of the @serve.zone/cloudly platform. This CLI is crafted to facilitate seamless interactions with complex cloud configurations and deployments, utilizing Docker Swarmkit orchestration.
|
**Command-line interface for Cloudly.** Manage your multi-cloud infrastructure from the terminal with powerful, intuitive commands.
|
||||||
|
|
||||||
## Install
|
## 🎯 What is @serve.zone/cli?
|
||||||
|
|
||||||
To begin using the `@serve.zone/cli` in your projects, install it via npm by running:
|
The Cloudly CLI brings the full power of the Cloudly platform to your terminal. Whether you're automating deployments, managing secrets, or monitoring services, the CLI provides a streamlined interface for all your cloud operations.
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- **⚡ Fast & Efficient** - Optimized for speed and minimal resource usage
|
||||||
|
- **🔐 Secure Authentication** - Token-based authentication with secure storage
|
||||||
|
- **📝 Intuitive Commands** - Clear, consistent command structure
|
||||||
|
- **🎨 Formatted Output** - Beautiful, readable output with color coding
|
||||||
|
- **🔄 Scriptable** - Perfect for CI/CD pipelines and automation
|
||||||
|
- **📊 Comprehensive** - Access to all Cloudly features from the terminal
|
||||||
|
|
||||||
|
## 🚀 Installation
|
||||||
|
|
||||||
|
### Global Installation (Recommended)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install @serve.zone/cli --save
|
pnpm add -g @serve.zone/cli
|
||||||
```
|
```
|
||||||
|
|
||||||
This command will download the package and integrate it into your project's `node_modules` directory, reflecting the dependency in your `package.json`.
|
### Local Installation
|
||||||
|
|
||||||
## Usage
|
```bash
|
||||||
|
pnpm add @serve.zone/cli
|
||||||
The `@serve.zone/cli` is a powerful command-line tool aimed at developers and system administrators who are managing containerized applications across various cloud platforms. Through this CLI, users can interact with their cloud infrastructure efficiently, enabling and extending `Cloudly’s` capabilities directly from the terminal.
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
Before proceeding to use the `@serve.zone/cli`, ensure your system meets the following prerequisites:
|
|
||||||
- Latest Node.js LTS version installed.
|
|
||||||
- Familiarity with basic command-line operations.
|
|
||||||
- Properly configured cloud service accounts (like Cloudflare, Hetzner), necessary for managing respective services.
|
|
||||||
|
|
||||||
### Setting Up the CLI
|
|
||||||
|
|
||||||
Begin setting up the `Cloudly` instance for CLI usage:
|
|
||||||
```typescript
|
|
||||||
// Import required modules
|
|
||||||
import { Cloudly } from '@serve.zone/cloudly';
|
|
||||||
import * as path from 'path';
|
|
||||||
|
|
||||||
// Define the configuration needed for cloud operations
|
|
||||||
const cloudlyConfig = {
|
|
||||||
cfToken: 'your-cloudflare-token',
|
|
||||||
hetznerToken: 'your-hetzner-token',
|
|
||||||
environment: 'production',
|
|
||||||
publicUrl: 'your-public-url',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Instantiate and start the Cloudly instance
|
|
||||||
const cloudlyInstance = new Cloudly(cloudlyConfig);
|
|
||||||
await cloudlyInstance.start();
|
|
||||||
|
|
||||||
// Log the setup information to ensure it’s correct
|
|
||||||
console.log(`Cloudly is set up at ${cloudlyInstance.config.data.publicUrl}`);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This snippet initializes a Cloudly instance with necessary environment configuration, setting the groundwork for all subsequent CLI operations.
|
## 🎬 Quick Start
|
||||||
|
|
||||||
### Core Operations with the CLI
|
```bash
|
||||||
|
# Configure your Cloudly instance
|
||||||
|
servezone config --url https://cloudly.example.com
|
||||||
|
|
||||||
Here's how you leverage various operational commands within the CLI feature:
|
# Login with your service token
|
||||||
|
servezone login --token your-service-token
|
||||||
|
|
||||||
#### Managing Clusters
|
# List your clusters
|
||||||
|
servezone clusters list
|
||||||
|
|
||||||
To create, list, and delete clusters, you’ll require invoking the `Cloudly` class with its cluster management logic:
|
# Deploy a service
|
||||||
|
servezone deploy --cluster production --image myapp:latest
|
||||||
```typescript
|
|
||||||
// Module imports
|
|
||||||
import { Cloudly } from '@serve.zone/cloudly';
|
|
||||||
|
|
||||||
// Async function for cluster management
|
|
||||||
async function manageCluster() {
|
|
||||||
// Prepare configuration
|
|
||||||
const config = {
|
|
||||||
cfToken: 'YOUR_CLOUDFLARE_TOKEN',
|
|
||||||
hetznerToken: 'YOUR_HETZNER_TOKEN',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize Cloudly
|
|
||||||
const cloudlyInstance = new Cloudly(config);
|
|
||||||
await cloudlyInstance.start();
|
|
||||||
|
|
||||||
// Example: Creating a new cluster
|
|
||||||
const cluster = await cloudlyInstance.clusterManager.createCluster({
|
|
||||||
id: 'example_cluster_id',
|
|
||||||
data: {
|
|
||||||
name: 'example_cluster',
|
|
||||||
servers: [],
|
|
||||||
sshKeys: [],
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Log cluster details
|
|
||||||
console.log('Cluster created:', cluster);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
With the above example, you can dynamically manage cluster configurations, ensuring your application components are effectively orchestrated across cloud environments.
|
|
||||||
|
|
||||||
#### Deploying Services
|
|
||||||
|
|
||||||
Deploying cloud-native services within your clusters can be achieved through the CLI:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Cloudly } from '@serve.zone/cloudly';
|
|
||||||
|
|
||||||
// Function to handle service deployment
|
|
||||||
async function deployService() {
|
|
||||||
const config = {
|
|
||||||
cfToken: 'YOUR_CLOUDFLARE_TOKEN',
|
|
||||||
hetznerToken: 'YOUR_HETZNER_TOKEN',
|
|
||||||
};
|
|
||||||
|
|
||||||
const cloudlyInstance = new Cloudly(config);
|
|
||||||
await cloudlyInstance.start();
|
|
||||||
|
|
||||||
// Deploy a new service to a specified cluster
|
|
||||||
const newService = {
|
|
||||||
id: 'example_service_id',
|
|
||||||
data: {
|
|
||||||
name: 'example_service',
|
|
||||||
imageId: 'example_image_id',
|
|
||||||
imageVersion: '1.0.0',
|
|
||||||
environment: {},
|
|
||||||
ports: { web: 80 }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Store service into database and deploy
|
|
||||||
console.log('Deploying service:', newService)
|
|
||||||
await cloudlyInstance.serverManager.deployService(newService);
|
|
||||||
}
|
|
||||||
|
|
||||||
deployService();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
By streamlining your service deployments through CLI, you ensure reproducibility and clarity in development operations.
|
## 🔑 Authentication
|
||||||
|
|
||||||
#### Managing Certificates
|
### Initial Setup
|
||||||
|
|
||||||
Ensuring secure connections by managing SSL certificates is essential. The CLI aids in this through Let's Encrypt integration:
|
```bash
|
||||||
|
# Set your Cloudly instance URL
|
||||||
|
servezone config --url https://cloudly.example.com
|
||||||
|
|
||||||
```typescript
|
# Authenticate with a service token
|
||||||
import { Cloudly } from '@serve.zone/cloudly';
|
servezone login --token YOUR_SERVICE_TOKEN
|
||||||
|
|
||||||
// Function to acquire a certificate
|
# Or use environment variables
|
||||||
async function getCertificate() {
|
export CLOUDLY_URL=https://cloudly.example.com
|
||||||
const config = {
|
export CLOUDLY_TOKEN=YOUR_SERVICE_TOKEN
|
||||||
cfToken: 'YOUR_CLOUDFLARE_TOKEN',
|
|
||||||
hetznerToken: 'YOUR_HETZNER_TOKEN',
|
|
||||||
};
|
|
||||||
|
|
||||||
const cloudlyInstance = new Cloudly(config);
|
|
||||||
await cloudlyInstance.start();
|
|
||||||
|
|
||||||
// Fetch certificate using Let's Encrypt
|
|
||||||
const domainName = 'example.com';
|
|
||||||
const cert = await cloudlyInstance.letsencryptConnector.getCertificateForDomain(domainName);
|
|
||||||
console.log(`Obtained certificate for domain ${domainName}:`, cert);
|
|
||||||
}
|
|
||||||
|
|
||||||
getCertificate();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This process facilitates the automation of SSL certificates provisioning, ensuring high security in your apps.
|
### Managing Profiles
|
||||||
|
|
||||||
### Automating Tasks with the CLI
|
```bash
|
||||||
|
# Create a profile for different environments
|
||||||
|
servezone profile create production --url https://prod.cloudly.com
|
||||||
|
servezone profile create staging --url https://stage.cloudly.com
|
||||||
|
|
||||||
Task scheduling is a feature you can utilize to automate recurring processes. Here’s an example of how `@serve.zone/cli` accomplishes task scheduling:
|
# Switch between profiles
|
||||||
|
servezone profile use production
|
||||||
|
|
||||||
```typescript
|
# List all profiles
|
||||||
import { TaskBuffer } from '@push.rocks/taskbuffer';
|
servezone profile list
|
||||||
|
|
||||||
// Schedule a task to run every day
|
|
||||||
const dailyTask = new TaskBuffer({
|
|
||||||
schedule: '0 0 * * *', // Using cron schedule
|
|
||||||
taskFunction: async () => {
|
|
||||||
console.log('Performing daily backup check...');
|
|
||||||
// Include backup logic here
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initiate task scheduling
|
|
||||||
dailyTask.start();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Scheduled tasks like periodic maintenance, data synchronization, or backups ensure you keep your cloud environment robust and reliable.
|
## 📚 Core Commands
|
||||||
|
|
||||||
### Integrating Third-Party APIs
|
### 🌐 Cluster Management
|
||||||
|
|
||||||
Expand the scope of your applications with API integrations offered via `@serve.zone/cli`:
|
```bash
|
||||||
|
# List all clusters
|
||||||
|
servezone clusters list
|
||||||
|
|
||||||
```typescript
|
# Get cluster details
|
||||||
import { Cloudly } from '@serve.zone/cloudly';
|
servezone clusters info production-cluster
|
||||||
|
|
||||||
// Function to send notifications
|
# Create a new cluster
|
||||||
async function sendNotification() {
|
servezone clusters create \
|
||||||
const cloudlyConfig = {
|
--name production-cluster \
|
||||||
cfToken: 'your-cloudflare-token',
|
--region eu-central \
|
||||||
hetznerToken: 'your-hetzner-token',
|
--nodes 3
|
||||||
};
|
|
||||||
|
|
||||||
const cloudly = new Cloudly(cloudlyConfig);
|
# Scale a cluster
|
||||||
await cloudly.start();
|
servezone clusters scale production-cluster --nodes 5
|
||||||
|
|
||||||
// Configure and send push notification
|
# Delete a cluster
|
||||||
await cloudly.externalApiManager.sendPushMessage({
|
servezone clusters delete staging-cluster
|
||||||
deviceToken: 'some_device_token',
|
|
||||||
message: 'Hello from Cloudly!',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
sendNotification();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
API integrations via the CLI extend Cloudly’s reach, enabling comprehensive service interconnections.
|
### 🐳 Service Deployment
|
||||||
|
|
||||||
### Security and Access Management
|
```bash
|
||||||
|
# Deploy a service
|
||||||
|
servezone deploy \
|
||||||
|
--cluster production \
|
||||||
|
--name api-service \
|
||||||
|
--image myapp:2.0.0 \
|
||||||
|
--replicas 3 \
|
||||||
|
--port 80:3000
|
||||||
|
|
||||||
Effective identity management is possible through `@serve.zone/cli`. Manage user roles, token validations, and more:
|
# Update a service
|
||||||
|
servezone service update api-service \
|
||||||
|
--image myapp:2.1.0 \
|
||||||
|
--replicas 5
|
||||||
|
|
||||||
```typescript
|
# Scale a service
|
||||||
import { Cloudly } from '@serve.zone/cloudly';
|
servezone service scale api-service --replicas 10
|
||||||
|
|
||||||
// Configuring and verifying identity
|
# Remove a service
|
||||||
async function authenticateUser() {
|
servezone service remove api-service
|
||||||
const cloudlyConfig = {
|
|
||||||
cfToken: 'your-cloudflare-token',
|
|
||||||
hetznerToken: 'your-hetzner-token',
|
|
||||||
};
|
|
||||||
|
|
||||||
const cloudly = new Cloudly(cloudlyConfig);
|
|
||||||
await cloudly.start();
|
|
||||||
|
|
||||||
// Sample user credentials
|
|
||||||
const userIdentity = {
|
|
||||||
userId: 'unique_user_id',
|
|
||||||
jwt: 'user_jwt_token',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Validate identity
|
|
||||||
const isValid = cloudly.authManager.validateIdentity(userIdentity);
|
|
||||||
console.log(`Is user identity valid? ${isValid}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
authenticateUser();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The applications of identity validation streamline operational security and enforce access controls across your systems.
|
### 🔐 Secret Management
|
||||||
|
|
||||||
These examples offer a glimpse into the vast potential of @serve.zone/cli, which combines automation, security, and flexibility for state-of-the-art cloud management. You are encouraged to build upon this documentation to harness Cloudly's full capabilities in your infrastructure and process ecosystems. Let the CLI transform your cloud management experience with precision and adaptability.
|
```bash
|
||||||
|
# Create a secret
|
||||||
|
servezone secrets create \
|
||||||
|
--name database-url \
|
||||||
|
--value "postgres://user:pass@host/db"
|
||||||
|
|
||||||
|
# Create a secret group
|
||||||
|
servezone secrets create-group \
|
||||||
|
--name api-secrets \
|
||||||
|
--secret DATABASE_URL=postgres://... \
|
||||||
|
--secret REDIS_URL=redis://...
|
||||||
|
|
||||||
|
# List secrets
|
||||||
|
servezone secrets list
|
||||||
|
|
||||||
|
# Get secret value
|
||||||
|
servezone secrets get database-url
|
||||||
|
|
||||||
|
# Delete a secret
|
||||||
|
servezone secrets delete old-secret
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📦 Image Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List images
|
||||||
|
servezone images list
|
||||||
|
|
||||||
|
# Push a new image
|
||||||
|
servezone images push \
|
||||||
|
--name myapp \
|
||||||
|
--version 2.0.0 \
|
||||||
|
--file ./myapp.tar
|
||||||
|
|
||||||
|
# Tag an image
|
||||||
|
servezone images tag myapp:2.0.0 myapp:latest
|
||||||
|
|
||||||
|
# Delete an image
|
||||||
|
servezone images delete myapp:1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📊 Monitoring & Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View service logs
|
||||||
|
servezone logs api-service
|
||||||
|
|
||||||
|
# Follow logs in real-time
|
||||||
|
servezone logs api-service --follow
|
||||||
|
|
||||||
|
# Filter logs
|
||||||
|
servezone logs api-service --since 1h --grep ERROR
|
||||||
|
|
||||||
|
# Get service status
|
||||||
|
servezone service status api-service
|
||||||
|
|
||||||
|
# Monitor cluster health
|
||||||
|
servezone clusters health production-cluster
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔧 DNS Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List DNS records
|
||||||
|
servezone dns list --domain example.com
|
||||||
|
|
||||||
|
# Create a DNS record
|
||||||
|
servezone dns create \
|
||||||
|
--domain example.com \
|
||||||
|
--name api \
|
||||||
|
--type A \
|
||||||
|
--value 192.168.1.1
|
||||||
|
|
||||||
|
# Update a DNS record
|
||||||
|
servezone dns update api.example.com --value 192.168.1.2
|
||||||
|
|
||||||
|
# Delete a DNS record
|
||||||
|
servezone dns delete old.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Advanced Usage
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set environment variables for a service
|
||||||
|
servezone deploy \
|
||||||
|
--name api-service \
|
||||||
|
--env NODE_ENV=production \
|
||||||
|
--env PORT=3000 \
|
||||||
|
--env DATABASE_URL=@secret:database-url
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Files
|
||||||
|
|
||||||
|
Create a `cloudly.yaml` file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
cluster: production
|
||||||
|
service:
|
||||||
|
name: api-service
|
||||||
|
image: myapp:latest
|
||||||
|
replicas: 3
|
||||||
|
ports:
|
||||||
|
- 80:3000
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
DATABASE_URL: "@secret:database-url"
|
||||||
|
```
|
||||||
|
|
||||||
|
Deploy using the config file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
servezone deploy --config cloudly.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Batch Operations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Deploy multiple services
|
||||||
|
servezone deploy --config services/*.yaml
|
||||||
|
|
||||||
|
# Update all services in a namespace
|
||||||
|
servezone service update --namespace api --image-tag v2.0.0
|
||||||
|
|
||||||
|
# Delete all staging resources
|
||||||
|
servezone cleanup --environment staging
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 CI/CD Integration
|
||||||
|
|
||||||
|
### GitHub Actions
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Deploy to Cloudly
|
||||||
|
run: |
|
||||||
|
servezone config --url ${{ secrets.CLOUDLY_URL }}
|
||||||
|
servezone login --token ${{ secrets.CLOUDLY_TOKEN }}
|
||||||
|
servezone deploy \
|
||||||
|
--cluster production \
|
||||||
|
--name api-service \
|
||||||
|
--image myapp:${{ github.sha }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GitLab CI
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
deploy:
|
||||||
|
script:
|
||||||
|
- servezone config --url $CLOUDLY_URL
|
||||||
|
- servezone login --token $CLOUDLY_TOKEN
|
||||||
|
- servezone deploy --config cloudly.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 Output Formats
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# JSON output for scripting
|
||||||
|
servezone clusters list --output json
|
||||||
|
|
||||||
|
# YAML output
|
||||||
|
servezone service info api-service --output yaml
|
||||||
|
|
||||||
|
# Table output (default)
|
||||||
|
servezone images list --output table
|
||||||
|
|
||||||
|
# Quiet mode (IDs only)
|
||||||
|
servezone clusters list --quiet
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Troubleshooting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enable debug output
|
||||||
|
servezone --debug clusters list
|
||||||
|
|
||||||
|
# Check CLI version
|
||||||
|
servezone version
|
||||||
|
|
||||||
|
# Test connection
|
||||||
|
servezone ping
|
||||||
|
|
||||||
|
# View configuration
|
||||||
|
servezone config show
|
||||||
|
|
||||||
|
# Clear cache and credentials
|
||||||
|
servezone logout --clear-cache
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Command Reference
|
||||||
|
|
||||||
|
```bash
|
||||||
|
servezone --help # Show all commands
|
||||||
|
servezone <command> --help # Show command-specific help
|
||||||
|
servezone clusters --help # Show cluster commands
|
||||||
|
servezone service --help # Show service commands
|
||||||
|
servezone secrets --help # Show secret commands
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔌 Shell Completion
|
||||||
|
|
||||||
|
Enable tab completion for your shell:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Bash
|
||||||
|
servezone completion bash > /etc/bash_completion.d/servezone
|
||||||
|
|
||||||
|
# Zsh
|
||||||
|
servezone completion zsh > ~/.zsh/completions/_servezone
|
||||||
|
|
||||||
|
# Fish
|
||||||
|
servezone completion fish > ~/.config/fish/completions/servezone.fish
|
||||||
|
```
|
||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
|
@@ -1,8 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/cli",
|
"name": "@serve.zone/cli",
|
||||||
"dependencies": [],
|
"dependencies": [
|
||||||
|
"@serve.zone/api",
|
||||||
|
"@serve.zone/interfaces",
|
||||||
|
"@push.rocks/projectinfo",
|
||||||
|
"@push.rocks/qenv",
|
||||||
|
"@push.rocks/smartcli"
|
||||||
|
],
|
||||||
"registries": [
|
"registries": [
|
||||||
"registry.npmjs.org:public",
|
"registry.npmjs.org:public",
|
||||||
"verdaccio.lossless.digital:public"
|
"verdaccio.lossless.digital:public"
|
||||||
]
|
],
|
||||||
|
"bin": ["servezone"]
|
||||||
}
|
}
|
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';
|
||||||
|
@@ -21,13 +21,19 @@ export interface ISecretBundle {
|
|||||||
*/
|
*/
|
||||||
type: 'service' | 'npmci' | 'gitzone' | 'external';
|
type: 'service' | 'npmci' | 'gitzone' | 'external';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set this if the secretBundle belongs to a service
|
||||||
|
*/
|
||||||
|
serviceId?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* You can add specific secret groups using this
|
* You can add specific secret groups using this
|
||||||
*/
|
*/
|
||||||
includedSecretGroupIds: string[];
|
includedSecretGroupIds: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* You can add specific tags using this
|
* access to this secretBundle also grants access to resources with matching tags
|
||||||
*/
|
*/
|
||||||
includedTags: {
|
includedTags: {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -35,9 +41,9 @@ export interface ISecretBundle {
|
|||||||
}[];
|
}[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* add images
|
* access to this secretBundle also grants access to the images
|
||||||
*/
|
*/
|
||||||
includedImages: {
|
imageClaims: {
|
||||||
imageId: string;
|
imageId: string;
|
||||||
permissions: ('read' | 'write')[];
|
permissions: ('read' | 'write')[];
|
||||||
}[];
|
}[];
|
||||||
|
@@ -8,7 +8,44 @@ export interface IService {
|
|||||||
imageId: string;
|
imageId: string;
|
||||||
imageVersion: string;
|
imageVersion: string;
|
||||||
environment: { [key: string]: string };
|
environment: { [key: string]: string };
|
||||||
|
/**
|
||||||
|
* the main secret bundle id, exclusive to the service
|
||||||
|
*/
|
||||||
secretBundleId: string;
|
secretBundleId: string;
|
||||||
|
/**
|
||||||
|
* those secret bundle ids do not belong to the service itself
|
||||||
|
* and thus live past the service lifecycle
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
};
|
@@ -2,9 +2,9 @@ import * as plugins from '../plugins.js';
|
|||||||
|
|
||||||
export type TTemplates = 'default' | 'linkaction' | 'notification';
|
export type TTemplates = 'default' | 'linkaction' | 'notification';
|
||||||
|
|
||||||
export interface IRequest_SendEmail extends plugins.typedrequestInterfaces.implementsTR<
|
export interface IReq_SendEmail extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
plugins.typedrequestInterfaces.ITypedRequest,
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
IRequest_SendEmail
|
IReq_SendEmail
|
||||||
> {
|
> {
|
||||||
method: 'sendEmail';
|
method: 'sendEmail';
|
||||||
request: {
|
request: {
|
||||||
@@ -25,9 +25,9 @@ export interface IRequest_SendEmail extends plugins.typedrequestInterfaces.imple
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IRequestRegisterRecipient extends plugins.typedrequestInterfaces.implementsTR<
|
export interface IReq_RegisterRecipient extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
plugins.typedrequestInterfaces.ITypedRequest,
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
IRequestRegisterRecipient
|
IReq_RegisterRecipient
|
||||||
> {
|
> {
|
||||||
method: 'registerRecepient';
|
method: 'registerRecepient';
|
||||||
request: {
|
request: {
|
||||||
@@ -37,3 +37,34 @@ export interface IRequestRegisterRecipient extends plugins.typedrequestInterface
|
|||||||
status: 'ok' | 'not ok';
|
status: 'ok' | 'not ok';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IReq_CheckEmailStatus extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_CheckEmailStatus
|
||||||
|
> {
|
||||||
|
method: 'checkEmailStatus';
|
||||||
|
request: {
|
||||||
|
emailId: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
status: string,
|
||||||
|
details?: { message: string; }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_GetEMailStats extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_GetEMailStats
|
||||||
|
> {
|
||||||
|
method: 'getEmailStats';
|
||||||
|
request: {
|
||||||
|
jwt: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
totalEmailsSent: number;
|
||||||
|
totalEmailsDelivered: number;
|
||||||
|
totalEmailsBounced: number;
|
||||||
|
averageDeliveryTimeMs: number;
|
||||||
|
lastUpdated: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@@ -1,207 +1,375 @@
|
|||||||
# @serve.zone/interfaces
|
# @serve.zone/interfaces 📋
|
||||||
|
|
||||||
interfaces for working with containers
|
**TypeScript interfaces for the Cloudly ecosystem.** Type-safe contracts for multi-cloud infrastructure management.
|
||||||
|
|
||||||
## Install
|
## 🎯 What is @serve.zone/interfaces?
|
||||||
|
|
||||||
To install `@serve.zone/interfaces`, run the following command in your terminal:
|
This package provides the complete set of TypeScript interfaces that power the Cloudly platform. It ensures type safety and consistency across all components - from API requests to data models, from service definitions to infrastructure configurations.
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- **🔒 Type Safety** - Comprehensive TypeScript interfaces for all Cloudly operations
|
||||||
|
- **📦 Modular Structure** - Organized by domain for easy navigation
|
||||||
|
- **🔄 Version Compatibility** - Interfaces versioned with the platform
|
||||||
|
- **📚 Well Documented** - Each interface includes JSDoc comments
|
||||||
|
- **🎭 Multi-Purpose** - Used by API clients, CLI tools, and web interfaces
|
||||||
|
- **✅ Validation Ready** - Compatible with runtime type checking libraries
|
||||||
|
|
||||||
|
## 🚀 Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install @serve.zone/interfaces --save
|
pnpm add @serve.zone/interfaces
|
||||||
```
|
```
|
||||||
|
|
||||||
This will add `@serve.zone/interfaces` to your project's dependencies, allowing you to import and use various predefined interfaces that facilitate container operations and interactions within the ServeZone ecosystem.
|
## 🏗️ Interface Categories
|
||||||
|
|
||||||
## Usage
|
### 📡 Request/Response Interfaces
|
||||||
|
|
||||||
The `@serve.zone/interfaces` module provides a robust set of TypeScript interfaces designed to standardize interaction with various services and components in a cloud-native environment. The interfaces are targeted at simplifying the integration process with container orchestration, network configurations, logging, and service definitions. The module is particularly useful if you're working on infrastructure or service orchestration solutions using Node.js and TypeScript.
|
Typed contracts for API communication:
|
||||||
|
|
||||||
This document guides you through a comprehensive use case scenario of `@serve.zone/interfaces`. We will cover how to effectively utilize these interfaces to set up cloud services, manage application configurations, and handle system-related communications. This tutorial will explore various feature sets within the module, focusing on real-world implementations and practical coding strategies.
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
Before diving in, make sure to satisfy the following prerequisites:
|
|
||||||
|
|
||||||
- **Node.js**: Ensure you have Node.js installed (preferably the latest LTS version).
|
|
||||||
|
|
||||||
- **TypeScript**: Your environment should support TypeScript, as this module leverages strong typing offered by TypeScript.
|
|
||||||
|
|
||||||
- **Cloud Account Access**: Some of the interfaces interact with live cloud services; thus, ensure you have necessary credentials (like API tokens) available for testing or integration.
|
|
||||||
|
|
||||||
### Core Interfaces and Scenarios
|
|
||||||
|
|
||||||
#### 1. Handling Typed Requests
|
|
||||||
|
|
||||||
One fundamental aspect is defining typed requests, which standardizes API call definitions across different microservices or components. The module offers interfaces such as `IRequest_GetAllImages`, `IRequest_CreateCluster`, that you can extend or implement within your service logic to ensure strong typing and consistency.
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { IRequest_GetAllImages } from '@serve.zone/interfaces/requests/image';
|
import {
|
||||||
|
IRequest_GetAllImages,
|
||||||
|
IRequest_CreateCluster,
|
||||||
|
IRequest_DeployService
|
||||||
|
} from '@serve.zone/interfaces';
|
||||||
|
|
||||||
class ImageService {
|
// Type-safe request
|
||||||
private cloudlyClient;
|
|
||||||
|
|
||||||
constructor(cloudlyClient: CloudlyApiClient) {
|
|
||||||
this.cloudlyClient = cloudlyClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async fetchAllImages() {
|
|
||||||
const request: IRequest_GetAllImages['request'] = {
|
const request: IRequest_GetAllImages['request'] = {
|
||||||
identity: this.cloudlyClient.identity,
|
identity: userIdentity,
|
||||||
|
filters: {
|
||||||
|
tag: 'production'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Type-safe response
|
||||||
|
const response: IRequest_GetAllImages['response'] = {
|
||||||
|
images: [...]
|
||||||
};
|
};
|
||||||
const response = await this.cloudlyClient.typedsocketClient.fireTypedRequest<IRequest_GetAllImages>(request);
|
|
||||||
return response.images;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
In the above code, we structured a simple function to retrieve all images from a service, assuming the `cloudlyClient` is your authenticated API client. The typed request interface ensures that both the request and response align with the expected types.
|
### 📦 Data Models
|
||||||
|
|
||||||
#### 2. Logging and Smart Logging Interfaces
|
Core data structures for Cloudly entities:
|
||||||
|
|
||||||
Logging is a crucial aspect of cloud applications. The module provides interfaces to assist in integrating logging systems like `@push.rocks/smartlog-interfaces`.
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { ILogger, ILogConfig } from '@push.rocks/smartlog-interfaces';
|
import {
|
||||||
|
ICluster,
|
||||||
|
IService,
|
||||||
|
IImage,
|
||||||
|
ISecret,
|
||||||
|
IServer
|
||||||
|
} from '@serve.zone/interfaces';
|
||||||
|
|
||||||
class LoggerService {
|
// Define a service
|
||||||
private logger: ILogger;
|
const service: IService = {
|
||||||
|
id: 'service-123',
|
||||||
constructor(logConfig: ILogConfig) {
|
|
||||||
this.logger = new SmartLogger(logConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
public logMessage(logPackage: ILogPackage) {
|
|
||||||
this.logger.log(logPackage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This illustrates a logger service utilizing `ILogConfig` to configure and initiate a logging mechanism. You can log structured data using `logPackage`, thus enhancing traceability and debugging efficiency.
|
|
||||||
|
|
||||||
#### 3. Container Service Management
|
|
||||||
|
|
||||||
Managing containers, particularly when dealing with microservices, can be complex, but interfaces like `IService`, `ICluster`, and `IServer` aid in structuring container service management.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { IService } from '@serve.zone/interfaces/data/service';
|
|
||||||
|
|
||||||
function defineService(): IService {
|
|
||||||
return {
|
|
||||||
id: 'unique-service-id',
|
|
||||||
data: {
|
data: {
|
||||||
name: 'my-container-service',
|
name: 'api-service',
|
||||||
imageId: 'unique-image-id',
|
imageId: 'image-456',
|
||||||
imageVersion: '1.0.0',
|
imageVersion: '2.0.0',
|
||||||
environment: { KEY: 'VALUE' },
|
environment: {
|
||||||
secretBundleId: 'bundle-id',
|
NODE_ENV: 'production'
|
||||||
scaleFactor: 2,
|
},
|
||||||
|
scaleFactor: 3,
|
||||||
balancingStrategy: 'round-robin',
|
balancingStrategy: 'round-robin',
|
||||||
ports: { web: 80 },
|
ports: {
|
||||||
domains: [{ name: 'example.com' }],
|
web: 80,
|
||||||
|
metrics: 9090
|
||||||
|
},
|
||||||
|
domains: [
|
||||||
|
{ name: 'api.example.com' }
|
||||||
|
],
|
||||||
deploymentIds: [],
|
deploymentIds: [],
|
||||||
deploymentDirectiveIds: [],
|
deploymentDirectiveIds: []
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
In the example, a service definition is drafted, encapsulating critical service metadata, including its environment variables, domain configuration, and load balancing strategy. Adhering to `IService` ensures that all necessary service data is encapsulated correctly.
|
### 🔐 Authentication & Identity
|
||||||
|
|
||||||
#### 4. Network Configuration and Routing
|
Identity management interfaces:
|
||||||
|
|
||||||
Networking is integral to cloud-native applications. Interfaces in `@serve.zone/interfaces` help shape network interaction patterns.
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { IReverseProxyConfig } from '@serve.zone/interfaces/data/traffic';
|
import {
|
||||||
|
IIdentity,
|
||||||
|
IServiceToken,
|
||||||
|
IPermission
|
||||||
|
} from '@serve.zone/interfaces';
|
||||||
|
|
||||||
|
const identity: IIdentity = {
|
||||||
|
id: 'user-789',
|
||||||
|
name: 'service-account',
|
||||||
|
type: 'service',
|
||||||
|
permissions: ['cluster:read', 'service:write'],
|
||||||
|
tokenHash: 'hashed-token',
|
||||||
|
metadata: {
|
||||||
|
createdAt: new Date(),
|
||||||
|
lastAccess: new Date()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🌐 Network Configuration
|
||||||
|
|
||||||
|
Networking and routing interfaces:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
IReverseProxyConfig,
|
||||||
|
IDomainConfig,
|
||||||
|
ILoadBalancerConfig
|
||||||
|
} from '@serve.zone/interfaces';
|
||||||
|
|
||||||
const proxyConfig: IReverseProxyConfig = {
|
const proxyConfig: IReverseProxyConfig = {
|
||||||
domain: 'example.com',
|
domain: 'app.example.com',
|
||||||
path: '/',
|
path: '/api',
|
||||||
serviceAddress: 'http://service:8080',
|
serviceAddress: 'http://api-service:3000',
|
||||||
ssl: true,
|
ssl: true,
|
||||||
};
|
headers: {
|
||||||
|
'X-Real-IP': '$remote_addr'
|
||||||
function configureProxy() {
|
|
||||||
// Logic to apply the proxyConfig, potentially using Typedi, Smartclient, or similar libraries.
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
Here, `IReverseProxyConfig` is used to define a reverse proxy for a service. Such configurations are necessary for routing external requests into internal services securely.
|
### 📊 Monitoring & Metrics
|
||||||
|
|
||||||
### Advanced Interface Utilization
|
Observability interfaces:
|
||||||
|
|
||||||
#### Monitoring and Metrics Collection
|
|
||||||
|
|
||||||
For observability, you can track system metrics using `IServerMetrics` or cluster status interfaces.
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { IServerMetrics } from '@serve.zone/interfaces/data/server';
|
import {
|
||||||
|
IServerMetrics,
|
||||||
|
IServiceMetrics,
|
||||||
|
IClusterHealth
|
||||||
|
} from '@serve.zone/interfaces';
|
||||||
|
|
||||||
function reportMetrics(metrics: IServerMetrics) {
|
const metrics: IServerMetrics = {
|
||||||
console.log(`CPU Usage: ${metrics.cpuUsageInPercent}%`);
|
serverId: 'server-001',
|
||||||
console.log(`Memory Usage: ${metrics.memoryUsageinMB}MB`);
|
cpuUsageInPercent: 65,
|
||||||
}
|
memoryUsageinMB: 3072,
|
||||||
|
memoryAvailableInMB: 8192,
|
||||||
const sampleMetrics: IServerMetrics = {
|
diskUsageInPercent: 40,
|
||||||
serverId: 'server-123',
|
networkInMbps: 100,
|
||||||
cpuUsageInPercent: 45,
|
networkOutMbps: 150,
|
||||||
memoryUsageinMB: 2048,
|
containerCount: 12,
|
||||||
memoryAvailableInMB: 4096,
|
containerMetrics: [...]
|
||||||
containerCount: 10,
|
|
||||||
containerMetrics: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
reportMetrics(sampleMetrics);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Implementing such metrics tracking provides insight into performance bottlenecks and helps strategize scaling decisions.
|
### 🔒 Secret Management
|
||||||
|
|
||||||
#### Certificate Management
|
Security and credential interfaces:
|
||||||
|
|
||||||
To handle SSL certificates programmatically, utilize interfaces such as `IRequest_Any_Cloudly_GetCertificateForDomain`.
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { IRequest_Any_Cloudly_GetCertificateForDomain } from '@serve.zone/interfaces/requests/certificate';
|
import {
|
||||||
|
ISecretGroup,
|
||||||
|
ISecretBundle,
|
||||||
|
IEncryptedData
|
||||||
|
} from '@serve.zone/interfaces';
|
||||||
|
|
||||||
async function fetchCertificate(cloudlyClient: CloudlyApiClient, domainName: string) {
|
const secretGroup: ISecretGroup = {
|
||||||
const request: IRequest_Any_Cloudly_GetCertificateForDomain['request'] = {
|
id: 'secrets-123',
|
||||||
identity: cloudlyClient.identity,
|
name: 'database-credentials',
|
||||||
domainName: domainName,
|
secrets: [
|
||||||
type: 'ssl'
|
{
|
||||||
};
|
key: 'DB_HOST',
|
||||||
|
value: 'encrypted-value',
|
||||||
return await cloudlyClient.typedsocketClient.fireTypedRequest<IRequest_Any_Cloudly_GetCertificateForDomain>(request);
|
encrypted: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'DB_PASSWORD',
|
||||||
|
value: 'encrypted-value',
|
||||||
|
encrypted: true
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
metadata: {
|
||||||
|
environment: 'production',
|
||||||
|
service: 'api'
|
||||||
|
}
|
||||||
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
Managing certificates dynamically via typed requests simplifies deployment and automates the security dimensions of your applications.
|
## 📚 Common Usage Patterns
|
||||||
|
|
||||||
#### Integrating with External Messaging Services
|
### Creating Type-Safe API Clients
|
||||||
|
|
||||||
Use `IRequest_SendEmail` to integrate platform services for sending emails:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { IRequest_SendEmail } from '@serve.zone/interfaces/platformservice/mta';
|
import {
|
||||||
|
IRequest_CreateService,
|
||||||
|
IService
|
||||||
|
} from '@serve.zone/interfaces';
|
||||||
|
|
||||||
async function sendNotification(emailClient: any) {
|
class ServiceClient {
|
||||||
const emailRequest: IRequest_SendEmail['request'] = {
|
async createService(
|
||||||
title: 'Welcome to ServeZone!',
|
serviceData: IService['data']
|
||||||
from: 'service@company.com',
|
): Promise<IService> {
|
||||||
to: 'user@example.com',
|
const request: IRequest_CreateService['request'] = {
|
||||||
body: '<h1>Congratulations</h1><p>Your account has been created successfully.</p>',
|
identity: this.identity,
|
||||||
|
serviceData
|
||||||
};
|
};
|
||||||
|
|
||||||
await emailClient.sendEmail(emailRequest);
|
const response = await this.client.send<IRequest_CreateService>(
|
||||||
|
'createService',
|
||||||
|
request
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.service;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
This approach demonstrates abstracting the email sending functionality using typed interfaces, contributing to code consistency and robustness.
|
### Validating Incoming Data
|
||||||
|
|
||||||
### Conclusion
|
```typescript
|
||||||
|
import { ICluster } from '@serve.zone/interfaces';
|
||||||
|
import { validateType } from 'your-validation-library';
|
||||||
|
|
||||||
The `@serve.zone/interfaces` module equips developers with a set of interfaces tailored for managing containers, orchestrating cloud services, and handling system interactions seamlessly. By applying these interfaces, projects can achieve coherence, reduce coupling, and simplify the integration process across various service domains.
|
function validateClusterData(data: unknown): ICluster {
|
||||||
|
if (!validateType<ICluster>(data)) {
|
||||||
|
throw new Error('Invalid cluster data');
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
Focusing on practical applications, try extending these interfaces to suit additional requirements in your projects. Engage actively with the module community, or contribute new ideas to enhance the breadth and depth of this interface library. Explore the integration patterns showcased here and contribute toward a sophisticated cloud-native development framework.
|
### Building Configuration Objects
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
ICloudlyConfig,
|
||||||
|
IMongoDescriptor
|
||||||
|
} from '@serve.zone/interfaces';
|
||||||
|
|
||||||
|
const config: ICloudlyConfig = {
|
||||||
|
cfToken: process.env.CF_TOKEN!,
|
||||||
|
hetznerToken: process.env.HETZNER_TOKEN!,
|
||||||
|
environment: 'production',
|
||||||
|
letsEncryptEmail: 'certs@example.com',
|
||||||
|
publicUrl: 'cloudly.example.com',
|
||||||
|
publicPort: 443,
|
||||||
|
mongoDescriptor: {
|
||||||
|
mongoDbUrl: process.env.MONGO_URL!,
|
||||||
|
mongoDbName: 'cloudly',
|
||||||
|
mongoDbUser: process.env.MONGO_USER!,
|
||||||
|
mongoDbPass: process.env.MONGO_PASS!
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Advanced Features
|
||||||
|
|
||||||
|
### Generic Request Handler
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ITypedRequest } from '@serve.zone/interfaces';
|
||||||
|
|
||||||
|
class RequestHandler {
|
||||||
|
async handle<T extends ITypedRequest>(
|
||||||
|
request: T['request']
|
||||||
|
): Promise<T['response']> {
|
||||||
|
// Type-safe request handling
|
||||||
|
return this.processRequest(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Discriminated Unions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { IDeploymentStatus } from '@serve.zone/interfaces';
|
||||||
|
|
||||||
|
function handleStatus(status: IDeploymentStatus) {
|
||||||
|
switch (status.type) {
|
||||||
|
case 'pending':
|
||||||
|
console.log('Deployment pending...');
|
||||||
|
break;
|
||||||
|
case 'running':
|
||||||
|
console.log(`Running on ${status.serverId}`);
|
||||||
|
break;
|
||||||
|
case 'failed':
|
||||||
|
console.log(`Failed: ${status.error}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Extending Interfaces
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { IService } from '@serve.zone/interfaces';
|
||||||
|
|
||||||
|
interface IExtendedService extends IService {
|
||||||
|
customMetadata: {
|
||||||
|
team: string;
|
||||||
|
costCenter: string;
|
||||||
|
sla: 'standard' | 'premium';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 Version Compatibility
|
||||||
|
|
||||||
|
| @serve.zone/interfaces | @serve.zone/cloudly | @serve.zone/api | @serve.zone/cli |
|
||||||
|
|------------------------|---------------------|-----------------|-----------------|
|
||||||
|
| 5.x | 5.x | 5.x | 5.x |
|
||||||
|
| 4.x | 4.x | 4.x | 4.x |
|
||||||
|
| 3.x | 3.x | 3.x | 3.x |
|
||||||
|
|
||||||
|
## 📖 Interface Documentation
|
||||||
|
|
||||||
|
All interfaces include comprehensive JSDoc comments:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* Represents a cluster configuration
|
||||||
|
* @interface ICluster
|
||||||
|
*/
|
||||||
|
export interface ICluster {
|
||||||
|
/**
|
||||||
|
* Unique identifier for the cluster
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cluster configuration data
|
||||||
|
* @type {IClusterData}
|
||||||
|
*/
|
||||||
|
data: IClusterData;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Development Tips
|
||||||
|
|
||||||
|
### Import Organization
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Group imports by category
|
||||||
|
import {
|
||||||
|
// Data models
|
||||||
|
ICluster,
|
||||||
|
IService,
|
||||||
|
IImage,
|
||||||
|
|
||||||
|
// Requests
|
||||||
|
IRequest_CreateCluster,
|
||||||
|
IRequest_DeployService,
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
ICloudlyConfig,
|
||||||
|
IReverseProxyConfig
|
||||||
|
} from '@serve.zone/interfaces';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type Guards
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { IService, ICluster } from '@serve.zone/interfaces';
|
||||||
|
|
||||||
|
function isService(entity: IService | ICluster): entity is IService {
|
||||||
|
return 'imageId' in entity.data;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
|
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;
|
||||||
|
@@ -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[];
|
||||||
|
};
|
||||||
|
}
|
@@ -40,24 +40,7 @@ extends plugins.typedrequestInterfaces.implementsTR<
|
|||||||
method: 'createService';
|
method: 'createService';
|
||||||
request: {
|
request: {
|
||||||
identity: IIdentity;
|
identity: IIdentity;
|
||||||
name: string;
|
serviceData: IService['data'];
|
||||||
description: string;
|
|
||||||
imageId: string;
|
|
||||||
imageVersion: string;
|
|
||||||
environment: { [key: string]: string };
|
|
||||||
secretBundleId: string;
|
|
||||||
scaleFactor: number;
|
|
||||||
balancingStrategy: 'round-robin' | 'least-connections';
|
|
||||||
ports: {
|
|
||||||
web: number;
|
|
||||||
custom?: { [domain: string]: string };
|
|
||||||
};
|
|
||||||
resources?: IServiceRessources;
|
|
||||||
domains: {
|
|
||||||
name: string;
|
|
||||||
port?: number;
|
|
||||||
protocol?: 'http' | 'https' | 'ssh';
|
|
||||||
}[];
|
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
service: IService;
|
service: IService;
|
||||||
@@ -73,36 +56,19 @@ extends plugins.typedrequestInterfaces.implementsTR<
|
|||||||
request: {
|
request: {
|
||||||
identity: IIdentity;
|
identity: IIdentity;
|
||||||
serviceId: string;
|
serviceId: string;
|
||||||
name: string;
|
serviceData: IService['data'];
|
||||||
description: string;
|
|
||||||
imageId: string;
|
|
||||||
imageVersion: string;
|
|
||||||
environment: { [key: string]: string };
|
|
||||||
secretBundleId: string;
|
|
||||||
scaleFactor: number;
|
|
||||||
balancingStrategy: 'round-robin' | 'least-connections';
|
|
||||||
ports: {
|
|
||||||
web: number;
|
|
||||||
custom?: { [domain: string]: string };
|
|
||||||
};
|
|
||||||
resources?: IServiceRessources;
|
|
||||||
domains: {
|
|
||||||
name: string;
|
|
||||||
port?: number;
|
|
||||||
protocol?: 'http' | 'https' | 'ssh';
|
|
||||||
}[];
|
|
||||||
};
|
};
|
||||||
response: {
|
response: {
|
||||||
service: IService;
|
service: IService;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IRequest_Any_Cloudly_DeleteService
|
export interface IRequest_Any_Cloudly_DeleteServiceById
|
||||||
extends plugins.typedrequestInterfaces.implementsTR<
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
plugins.typedrequestInterfaces.ITypedRequest,
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
IRequest_Any_Cloudly_DeleteService
|
IRequest_Any_Cloudly_DeleteServiceById
|
||||||
> {
|
> {
|
||||||
method: 'deleteService';
|
method: 'deleteServiceById';
|
||||||
request: {
|
request: {
|
||||||
identity: IIdentity;
|
identity: IIdentity;
|
||||||
serviceId: string;
|
serviceId: string;
|
||||||
@@ -111,3 +77,19 @@ extends plugins.typedrequestInterfaces.implementsTR<
|
|||||||
success: boolean;
|
success: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IRequest_Any_Cloudly_GetServiceSecretBundlesAsFlatObject
|
||||||
|
extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IRequest_Any_Cloudly_GetServiceSecretBundlesAsFlatObject
|
||||||
|
> {
|
||||||
|
method: 'getServiceSecretBundlesAsFlatObject';
|
||||||
|
request: {
|
||||||
|
identity: IIdentity;
|
||||||
|
serviceId: string;
|
||||||
|
environment: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
flatKeyValueObject: {[key: string]: string};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
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: '4.11.0',
|
version: '5.2.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.'
|
||||||
}
|
}
|
||||||
|
@@ -245,6 +245,7 @@ export const addClusterAction = dataState.createAction(
|
|||||||
statePartArg,
|
statePartArg,
|
||||||
payloadArg: {
|
payloadArg: {
|
||||||
clusterName: string;
|
clusterName: string;
|
||||||
|
setupMode?: 'manual' | 'hetzner' | 'aws' | 'digitalocean';
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
let currentState = statePartArg.getState();
|
let currentState = statePartArg.getState();
|
||||||
|
@@ -24,6 +24,8 @@ import { CloudlyViewS3 } from './cloudly-view-s3.js';
|
|||||||
import { CloudlyViewSecretBundles } from './cloudly-view-secretbundles.js';
|
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 { CloudlyViewSettings } from './cloudly-view-settings.js';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -75,62 +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',
|
||||||
|
iconName: 'lucide:Package',
|
||||||
|
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;
|
||||||
|
129
ts_web/elements/cloudly-view-externalregistries.ts
Normal file
129
ts_web/elements/cloudly-view-externalregistries.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import * as shared from '../elements/shared/index.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
state,
|
||||||
|
css,
|
||||||
|
cssManager,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
|
||||||
|
import * as appstate from '../appstate.js';
|
||||||
|
|
||||||
|
@customElement('cloudly-view-externalregistries')
|
||||||
|
export class CloudlyViewExternalRegistries extends DeesElement {
|
||||||
|
@state()
|
||||||
|
private data: appstate.IDataState = {
|
||||||
|
secretGroups: [],
|
||||||
|
secretBundles: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const subecription = appstate.dataState
|
||||||
|
.select((stateArg) => stateArg)
|
||||||
|
.subscribe((dataArg) => {
|
||||||
|
this.data = dataArg;
|
||||||
|
});
|
||||||
|
this.rxSubscriptions.push(subecription);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
shared.viewHostCss,
|
||||||
|
css`
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return html`
|
||||||
|
<cloudly-sectionheading>External Registries</cloudly-sectionheading>
|
||||||
|
<dees-table
|
||||||
|
.heading1=${'External Registries'}
|
||||||
|
.heading2=${'decoded in client'}
|
||||||
|
.data=${this.data.deployments}
|
||||||
|
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
|
||||||
|
return {
|
||||||
|
id: itemArg.id,
|
||||||
|
serverAmount: itemArg.data.servers.length,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
.dataActions=${[
|
||||||
|
{
|
||||||
|
name: 'add configBundle',
|
||||||
|
iconName: 'plus',
|
||||||
|
type: ['header', 'footer'],
|
||||||
|
actionFunc: async (dataActionArg) => {
|
||||||
|
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
|
heading: 'Add ConfigBundle',
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'data.secretGroupIds'}
|
||||||
|
.label=${'secretGroupIds'}
|
||||||
|
.value=${''}
|
||||||
|
></dees-input-text>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'data.includedTags'}
|
||||||
|
.label=${'includedTags'}
|
||||||
|
.value=${''}
|
||||||
|
></dees-input-text>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'create', action: async (modalArg) => {} },
|
||||||
|
{
|
||||||
|
name: 'cancel',
|
||||||
|
action: async (modalArg) => {
|
||||||
|
modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'delete',
|
||||||
|
iconName: 'trash',
|
||||||
|
type: ['contextmenu', 'inRow'],
|
||||||
|
actionFunc: async (actionDataArg) => {
|
||||||
|
plugins.deesCatalog.DeesModal.createAndShow({
|
||||||
|
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
|
||||||
|
content: html`
|
||||||
|
<div style="text-align:center">
|
||||||
|
Do you really want to delete the ConfigBundle?
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
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;"
|
||||||
|
>
|
||||||
|
${actionDataArg.item.id}
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: 'cancel',
|
||||||
|
action: async (modalArg) => {
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'delete',
|
||||||
|
action: async (modalArg) => {
|
||||||
|
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
|
||||||
|
configBundleId: actionDataArg.item.id,
|
||||||
|
});
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as plugins.deesCatalog.ITableAction[]}
|
||||||
|
></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>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
@@ -18,11 +18,28 @@ export class CloudlySectionheading extends DeesElement {
|
|||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
css`
|
css`
|
||||||
|
:host {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: min-content min-content;
|
||||||
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-family: 'Cal Sans';
|
font-family: 'Cal Sans';
|
||||||
letter-spacing: 0.025em;
|
letter-spacing: 0.025em;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag {
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #8a0183;
|
||||||
|
height: 20px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
margin-left: 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
transform: translateY(12px);
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
]
|
]
|
||||||
@@ -30,6 +47,7 @@ export class CloudlySectionheading extends DeesElement {
|
|||||||
public render() {
|
public render() {
|
||||||
return html`
|
return html`
|
||||||
<h1><slot></slot></h1>
|
<h1><slot></slot></h1>
|
||||||
|
<div class="flag">stability: alpha</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user