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:
11
changelog.md
11
changelog.md
@@ -1,5 +1,16 @@
|
||||
# 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)
|
||||
Improve Let's Encrypt integration and certificate handling; fix coreflow certificate response; add local assistant permissions config
|
||||
|
||||
|
16
package.json
16
package.json
@@ -22,24 +22,24 @@
|
||||
"docs": "tsdoc aidoc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^2.6.7",
|
||||
"@git.zone/tsbuild": "^2.6.8",
|
||||
"@git.zone/tsbundle": "^2.5.1",
|
||||
"@git.zone/tsdoc": "^1.5.1",
|
||||
"@git.zone/tspublish": "^1.10.3",
|
||||
"@git.zone/tstest": "^2.3.5",
|
||||
"@git.zone/tstest": "^2.3.6",
|
||||
"@git.zone/tswatch": "^2.2.1",
|
||||
"@types/node": "^22.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@api.global/typedrequest": "3.1.10",
|
||||
"@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",
|
||||
"@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/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-element": "^2.1.2",
|
||||
"@git.zone/tsrun": "^1.3.3",
|
||||
@@ -59,19 +59,19 @@
|
||||
"@push.rocks/smartguard": "^3.1.0",
|
||||
"@push.rocks/smartjson": "^5.0.19",
|
||||
"@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-interfaces": "^3.0.2",
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
"@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/smartssh": "^2.0.1",
|
||||
"@push.rocks/smartstate": "^2.0.26",
|
||||
"@push.rocks/smartstream": "^3.2.5",
|
||||
"@push.rocks/smartstring": "^4.0.15",
|
||||
"@push.rocks/smartunique": "^3.0.9",
|
||||
"@push.rocks/taskbuffer": "^3.0.2",
|
||||
"@push.rocks/taskbuffer": "^3.1.10",
|
||||
"@push.rocks/webjwt": "^1.0.9",
|
||||
"@tsclass/tsclass": "^9.2.0"
|
||||
},
|
||||
|
491
pnpm-lock.yaml
generated
491
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
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.'
|
||||
}
|
||||
|
@@ -24,11 +24,13 @@ export class ClusterManager {
|
||||
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.cluster.IRequest_CreateCluster>(
|
||||
new plugins.typedrequest.TypedHandler('createCluster', async (dataArg) => {
|
||||
// TODO: guards
|
||||
const setupMode = dataArg.setupMode || 'manual'; // Default to manual if not specified
|
||||
const cluster = await this.createCluster({
|
||||
id: plugins.smartunique.uniSimple('cluster'),
|
||||
data: {
|
||||
userId: null, // this is created by the createCluster method
|
||||
name: dataArg.clusterName,
|
||||
setupMode: setupMode,
|
||||
acmeInfo: null,
|
||||
cloudlyUrl: `https://${this.cloudlyRef.config.data.publicUrl}:${this.cloudlyRef.config.data.publicPort}/`,
|
||||
servers: [],
|
||||
@@ -36,7 +38,12 @@ export class ClusterManager {
|
||||
},
|
||||
});
|
||||
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 {
|
||||
cluster: await cluster.createSavableObject(),
|
||||
};
|
||||
|
@@ -55,6 +55,12 @@ export class CloudlyServerManager {
|
||||
// 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 server provisioning for cluster ${cluster.id} - setupMode is ${cluster.data.setupMode || 'manual'}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// get existing servers
|
||||
const servers = await this.getServersByCluster(cluster);
|
||||
|
||||
|
@@ -18,6 +18,11 @@ export interface ICluster {
|
||||
*/
|
||||
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
|
||||
*/
|
||||
|
@@ -41,6 +41,7 @@ export interface IRequest_CreateCluster extends plugins.typedrequestInterfaces.i
|
||||
request: {
|
||||
identity: userInterfaces.IIdentity;
|
||||
clusterName: string;
|
||||
setupMode?: 'manual' | 'hetzner' | 'aws' | 'digitalocean';
|
||||
};
|
||||
response: {
|
||||
cluster: clusterInterfaces.ICluster;
|
||||
|
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
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.'
|
||||
}
|
||||
|
@@ -245,6 +245,7 @@ export const addClusterAction = dataState.createAction(
|
||||
statePartArg,
|
||||
payloadArg: {
|
||||
clusterName: string;
|
||||
setupMode?: 'manual' | 'hetzner' | 'aws' | 'digitalocean';
|
||||
}
|
||||
) => {
|
||||
let currentState = statePartArg.getState();
|
||||
|
@@ -76,66 +76,82 @@ export class CloudlyDashboard extends DeesElement {
|
||||
.viewTabs=${[
|
||||
{
|
||||
name: 'Overview',
|
||||
iconName: 'lucide:LayoutDashboard',
|
||||
element: CloudlyViewOverview,
|
||||
},
|
||||
{
|
||||
name: 'SecretGroups',
|
||||
iconName: 'lucide:ShieldCheck',
|
||||
element: CloudlyViewSecretGroups,
|
||||
},
|
||||
{
|
||||
name: 'SecretBundles',
|
||||
iconName: 'lucide:LockKeyhole',
|
||||
element: CloudlyViewSecretBundles,
|
||||
},
|
||||
{
|
||||
name: 'Clusters',
|
||||
iconName: 'lucide:Network',
|
||||
element: CloudlyViewClusters,
|
||||
},
|
||||
{
|
||||
name: 'ExternalRegistries',
|
||||
iconName: 'lucide:Package',
|
||||
element: CloudlyViewExternalRegistries,
|
||||
},
|
||||
{
|
||||
name: 'Images',
|
||||
iconName: 'lucide:Image',
|
||||
element: CloudlyViewImages,
|
||||
},
|
||||
{
|
||||
name: 'Services',
|
||||
iconName: 'lucide:Layers',
|
||||
element: CloudlyViewServices,
|
||||
},
|
||||
{
|
||||
name: 'Testing & Building',
|
||||
iconName: 'lucide:HardHat',
|
||||
element: CloudlyViewServices,
|
||||
},
|
||||
{
|
||||
name: 'Deployments',
|
||||
iconName: 'lucide:Rocket',
|
||||
element: CloudlyViewDeployments,
|
||||
},
|
||||
{
|
||||
name: 'DNS',
|
||||
iconName: 'lucide:Globe',
|
||||
element: CloudlyViewDns,
|
||||
},
|
||||
{
|
||||
name: 'Mails',
|
||||
iconName: 'lucide:Mail',
|
||||
element: CloudlyViewMails,
|
||||
},
|
||||
{
|
||||
name: 'Logs',
|
||||
iconName: 'lucide:FileText',
|
||||
element: CloudlyViewLogs,
|
||||
},
|
||||
{
|
||||
name: 's3',
|
||||
iconName: 'lucide:Cloud',
|
||||
element: CloudlyViewS3,
|
||||
},
|
||||
{
|
||||
name: 'DBs',
|
||||
iconName: 'lucide:Database',
|
||||
element: CloudlyViewDbs,
|
||||
},
|
||||
{
|
||||
name: 'Backups',
|
||||
iconName: 'lucide:Save',
|
||||
element: CloudlyViewBackups,
|
||||
},
|
||||
{
|
||||
name: 'Fleet',
|
||||
iconName: 'lucide:Truck',
|
||||
element: CloudlyViewBackups,
|
||||
}
|
||||
] as plugins.deesCatalog.IView[]}
|
||||
|
@@ -68,6 +68,18 @@ export class CloudlyViewClusters extends DeesElement {
|
||||
.description=${'a descriptive name for the cluster'}
|
||||
.value=${''}
|
||||
></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>
|
||||
`,
|
||||
menuOptions: [
|
||||
@@ -76,6 +88,7 @@ export class CloudlyViewClusters extends DeesElement {
|
||||
action: async (modalArg) => {
|
||||
const data: {
|
||||
clusterName: string;
|
||||
setupMode: 'manual' | 'hetzner' | 'aws' | 'digitalocean';
|
||||
} = (await modalArg.shadowRoot
|
||||
.querySelector('dees-form')
|
||||
.collectFormData()) as any;
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as shared from '../elements/shared/index.js';
|
||||
|
||||
import {
|
||||
@@ -34,34 +33,124 @@ export class CloudlyViewOverview extends DeesElement {
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css`
|
||||
.clusterGrid {
|
||||
display: grid;
|
||||
grid-template-columns: ${cssManager.cssGridColumns(3, 8)};
|
||||
grid-gap: 16px;
|
||||
margin-bottom: 40px;
|
||||
dees-statsgrid {
|
||||
margin-top: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
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`
|
||||
<cloudly-sectionheading>Overview</cloudly-sectionheading>
|
||||
${this.data.clusters.length === 0 ? html`
|
||||
You need to create at least one cluster to see an overview.
|
||||
`: html``}
|
||||
${this.data.clusters.map(
|
||||
(clusterArg) => html`
|
||||
<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>
|
||||
`
|
||||
)}
|
||||
<dees-statsgrid
|
||||
.tiles=${statsTiles}
|
||||
.minTileWidth=${250}
|
||||
.gap=${16}
|
||||
></dees-statsgrid>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user