feat(cluster): Add cluster setupMode (manual|hetzner|aws|digitalocean) with conditional Hetzner auto-provisioning; UI and dashboard improvements; dependency upgrades

This commit is contained in:
2025-09-05 16:07:46 +00:00
parent 330797ab1a
commit eefaa55e13
13 changed files with 392 additions and 314 deletions

View File

@@ -1,5 +1,16 @@
# Changelog # Changelog
## 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) ## 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 Improve Let's Encrypt integration and certificate handling; fix coreflow certificate response; add local assistant permissions config

View File

@@ -22,24 +22,24 @@
"docs": "tsdoc aidoc" "docs": "tsdoc aidoc"
}, },
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^2.6.7", "@git.zone/tsbuild": "^2.6.8",
"@git.zone/tsbundle": "^2.5.1", "@git.zone/tsbundle": "^2.5.1",
"@git.zone/tsdoc": "^1.5.1", "@git.zone/tsdoc": "^1.5.1",
"@git.zone/tspublish": "^1.10.3", "@git.zone/tspublish": "^1.10.3",
"@git.zone/tstest": "^2.3.5", "@git.zone/tstest": "^2.3.6",
"@git.zone/tswatch": "^2.2.1", "@git.zone/tswatch": "^2.2.1",
"@types/node": "^22.0.0" "@types/node": "^22.0.0"
}, },
"dependencies": { "dependencies": {
"@api.global/typedrequest": "3.1.10", "@api.global/typedrequest": "3.1.10",
"@api.global/typedrequest-interfaces": "^3.0.19", "@api.global/typedrequest-interfaces": "^3.0.19",
"@api.global/typedserver": "^3.0.77", "@api.global/typedserver": "^3.0.79",
"@api.global/typedsocket": "^3.0.1", "@api.global/typedsocket": "^3.0.1",
"@apiclient.xyz/cloudflare": "^6.4.1", "@apiclient.xyz/cloudflare": "^6.4.1",
"@apiclient.xyz/docker": "^1.3.0", "@apiclient.xyz/docker": "^1.3.5",
"@apiclient.xyz/hetznercloud": "^1.2.0", "@apiclient.xyz/hetznercloud": "^1.2.0",
"@apiclient.xyz/slack": "^3.0.9", "@apiclient.xyz/slack": "^3.0.9",
"@design.estate/dees-catalog": "^1.10.10", "@design.estate/dees-catalog": "^1.10.12",
"@design.estate/dees-domtools": "^2.3.3", "@design.estate/dees-domtools": "^2.3.3",
"@design.estate/dees-element": "^2.1.2", "@design.estate/dees-element": "^2.1.2",
"@git.zone/tsrun": "^1.3.3", "@git.zone/tsrun": "^1.3.3",
@@ -59,19 +59,19 @@
"@push.rocks/smartguard": "^3.1.0", "@push.rocks/smartguard": "^3.1.0",
"@push.rocks/smartjson": "^5.0.19", "@push.rocks/smartjson": "^5.0.19",
"@push.rocks/smartjwt": "^2.2.1", "@push.rocks/smartjwt": "^2.2.1",
"@push.rocks/smartlog": "^3.1.8", "@push.rocks/smartlog": "^3.1.9",
"@push.rocks/smartlog-destination-clickhouse": "^1.0.13", "@push.rocks/smartlog-destination-clickhouse": "^1.0.13",
"@push.rocks/smartlog-interfaces": "^3.0.2", "@push.rocks/smartlog-interfaces": "^3.0.2",
"@push.rocks/smartpath": "^6.0.0", "@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.2.3", "@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartrequest": "^4.2.2", "@push.rocks/smartrequest": "^4.3.1",
"@push.rocks/smartrx": "^3.0.10", "@push.rocks/smartrx": "^3.0.10",
"@push.rocks/smartssh": "^2.0.1", "@push.rocks/smartssh": "^2.0.1",
"@push.rocks/smartstate": "^2.0.26", "@push.rocks/smartstate": "^2.0.26",
"@push.rocks/smartstream": "^3.2.5", "@push.rocks/smartstream": "^3.2.5",
"@push.rocks/smartstring": "^4.0.15", "@push.rocks/smartstring": "^4.0.15",
"@push.rocks/smartunique": "^3.0.9", "@push.rocks/smartunique": "^3.0.9",
"@push.rocks/taskbuffer": "^3.0.2", "@push.rocks/taskbuffer": "^3.1.10",
"@push.rocks/webjwt": "^1.0.9", "@push.rocks/webjwt": "^1.0.9",
"@tsclass/tsclass": "^9.2.0" "@tsclass/tsclass": "^9.2.0"
}, },

491
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/cloudly', name: '@serve.zone/cloudly',
version: '5.0.6', version: '5.1.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.'
} }

View File

@@ -24,11 +24,13 @@ 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: [], servers: [],
@@ -36,7 +38,12 @@ export class ClusterManager {
}, },
}); });
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.serverManager.ensureServerInfrastructure();
}
return { return {
cluster: await cluster.createSavableObject(), cluster: await cluster.createSavableObject(),
}; };

View File

@@ -55,6 +55,12 @@ export class CloudlyServerManager {
// get all clusters // get all clusters
const allClusters = await this.cloudlyRef.clusterManager.getAllClusters(); const allClusters = await this.cloudlyRef.clusterManager.getAllClusters();
for (const cluster of allClusters) { for (const cluster of allClusters) {
// Skip clusters that are not set up for Hetzner auto-provisioning
if (cluster.data.setupMode !== 'hetzner') {
console.log(`Skipping server provisioning for cluster ${cluster.id} - setupMode is ${cluster.data.setupMode || 'manual'}`);
continue;
}
// get existing servers // get existing servers
const servers = await this.getServersByCluster(cluster); const servers = await this.getServersByCluster(cluster);

View File

@@ -18,6 +18,11 @@ export interface ICluster {
*/ */
cloudlyUrl?: string; cloudlyUrl?: string;
/**
* Cluster setup mode - manual by default, or auto-provision with cloud provider
*/
setupMode?: 'manual' | 'hetzner' | 'aws' | 'digitalocean';
/** /**
* what servers are expected to be part of the cluster * what servers are expected to be part of the cluster
*/ */

View File

@@ -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;

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/cloudly', name: '@serve.zone/cloudly',
version: '5.0.6', version: '5.1.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.'
} }

View File

@@ -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();

View File

@@ -76,66 +76,82 @@ export class CloudlyDashboard extends DeesElement {
.viewTabs=${[ .viewTabs=${[
{ {
name: 'Overview', name: 'Overview',
iconName: 'lucide:LayoutDashboard',
element: CloudlyViewOverview, element: CloudlyViewOverview,
}, },
{ {
name: 'SecretGroups', name: 'SecretGroups',
iconName: 'lucide:ShieldCheck',
element: CloudlyViewSecretGroups, element: CloudlyViewSecretGroups,
}, },
{ {
name: 'SecretBundles', name: 'SecretBundles',
iconName: 'lucide:LockKeyhole',
element: CloudlyViewSecretBundles, element: CloudlyViewSecretBundles,
}, },
{ {
name: 'Clusters', name: 'Clusters',
iconName: 'lucide:Network',
element: CloudlyViewClusters, element: CloudlyViewClusters,
}, },
{ {
name: 'ExternalRegistries', name: 'ExternalRegistries',
iconName: 'lucide:Package',
element: CloudlyViewExternalRegistries, element: CloudlyViewExternalRegistries,
}, },
{ {
name: 'Images', name: 'Images',
iconName: 'lucide:Image',
element: CloudlyViewImages, element: CloudlyViewImages,
}, },
{ {
name: 'Services', name: 'Services',
iconName: 'lucide:Layers',
element: CloudlyViewServices, element: CloudlyViewServices,
}, },
{ {
name: 'Testing & Building', name: 'Testing & Building',
iconName: 'lucide:HardHat',
element: CloudlyViewServices, element: CloudlyViewServices,
}, },
{ {
name: 'Deployments', name: 'Deployments',
iconName: 'lucide:Rocket',
element: CloudlyViewDeployments, element: CloudlyViewDeployments,
}, },
{ {
name: 'DNS', name: 'DNS',
iconName: 'lucide:Globe',
element: CloudlyViewDns, element: CloudlyViewDns,
}, },
{ {
name: 'Mails', name: 'Mails',
iconName: 'lucide:Mail',
element: CloudlyViewMails, element: CloudlyViewMails,
}, },
{ {
name: 'Logs', name: 'Logs',
iconName: 'lucide:FileText',
element: CloudlyViewLogs, element: CloudlyViewLogs,
}, },
{ {
name: 's3', name: 's3',
iconName: 'lucide:Cloud',
element: CloudlyViewS3, element: CloudlyViewS3,
}, },
{ {
name: 'DBs', name: 'DBs',
iconName: 'lucide:Database',
element: CloudlyViewDbs, element: CloudlyViewDbs,
}, },
{ {
name: 'Backups', name: 'Backups',
iconName: 'lucide:Save',
element: CloudlyViewBackups, element: CloudlyViewBackups,
}, },
{ {
name: 'Fleet', name: 'Fleet',
iconName: 'lucide:Truck',
element: CloudlyViewBackups, element: CloudlyViewBackups,
} }
] as plugins.deesCatalog.IView[]} ] as plugins.deesCatalog.IView[]}

View File

@@ -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;

View File

@@ -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 servers across all clusters
const totalServers = this.data.clusters?.reduce((sum, cluster) =>
sum + (cluster.data.servers?.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: 'servers',
title: 'Total Servers',
value: totalServers,
type: 'number' as const,
iconName: 'lucide:Server',
description: 'Connected servers'
},
{
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>
`
)}
`; `;
} }
} }