s3db.js 13.4.0 → 13.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -10
- package/dist/{s3db.cjs.js → s3db.cjs} +38801 -32446
- package/dist/s3db.cjs.map +1 -0
- package/dist/s3db.es.js +38653 -32291
- package/dist/s3db.es.js.map +1 -1
- package/package.json +218 -22
- package/src/concerns/id.js +90 -6
- package/src/concerns/index.js +2 -1
- package/src/concerns/password-hashing.js +150 -0
- package/src/database.class.js +6 -2
- package/src/plugins/api/auth/basic-auth.js +40 -10
- package/src/plugins/api/auth/index.js +49 -3
- package/src/plugins/api/auth/oauth2-auth.js +171 -0
- package/src/plugins/api/auth/oidc-auth.js +789 -0
- package/src/plugins/api/auth/oidc-client.js +462 -0
- package/src/plugins/api/auth/path-auth-matcher.js +284 -0
- package/src/plugins/api/concerns/event-emitter.js +134 -0
- package/src/plugins/api/concerns/failban-manager.js +651 -0
- package/src/plugins/api/concerns/guards-helpers.js +402 -0
- package/src/plugins/api/concerns/metrics-collector.js +346 -0
- package/src/plugins/api/index.js +510 -57
- package/src/plugins/api/middlewares/failban.js +305 -0
- package/src/plugins/api/middlewares/rate-limit.js +301 -0
- package/src/plugins/api/middlewares/request-id.js +74 -0
- package/src/plugins/api/middlewares/security-headers.js +120 -0
- package/src/plugins/api/middlewares/session-tracking.js +194 -0
- package/src/plugins/api/routes/auth-routes.js +119 -78
- package/src/plugins/api/routes/resource-routes.js +73 -30
- package/src/plugins/api/server.js +1139 -45
- package/src/plugins/api/utils/custom-routes.js +102 -0
- package/src/plugins/api/utils/guards.js +213 -0
- package/src/plugins/api/utils/mime-types.js +154 -0
- package/src/plugins/api/utils/openapi-generator.js +91 -12
- package/src/plugins/api/utils/path-matcher.js +173 -0
- package/src/plugins/api/utils/static-filesystem.js +262 -0
- package/src/plugins/api/utils/static-s3.js +231 -0
- package/src/plugins/api/utils/template-engine.js +188 -0
- package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +853 -0
- package/src/plugins/cloud-inventory/drivers/aws-driver.js +2554 -0
- package/src/plugins/cloud-inventory/drivers/azure-driver.js +637 -0
- package/src/plugins/cloud-inventory/drivers/base-driver.js +99 -0
- package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +620 -0
- package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +698 -0
- package/src/plugins/cloud-inventory/drivers/gcp-driver.js +645 -0
- package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +559 -0
- package/src/plugins/cloud-inventory/drivers/linode-driver.js +614 -0
- package/src/plugins/cloud-inventory/drivers/mock-drivers.js +449 -0
- package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +771 -0
- package/src/plugins/cloud-inventory/drivers/oracle-driver.js +768 -0
- package/src/plugins/cloud-inventory/drivers/vultr-driver.js +636 -0
- package/src/plugins/cloud-inventory/index.js +20 -0
- package/src/plugins/cloud-inventory/registry.js +146 -0
- package/src/plugins/cloud-inventory/terraform-exporter.js +362 -0
- package/src/plugins/cloud-inventory.plugin.js +1333 -0
- package/src/plugins/concerns/plugin-dependencies.js +62 -2
- package/src/plugins/eventual-consistency/analytics.js +1 -0
- package/src/plugins/eventual-consistency/consolidation.js +2 -2
- package/src/plugins/eventual-consistency/garbage-collection.js +2 -2
- package/src/plugins/eventual-consistency/install.js +2 -2
- package/src/plugins/identity/README.md +335 -0
- package/src/plugins/identity/concerns/mfa-manager.js +204 -0
- package/src/plugins/identity/concerns/password.js +138 -0
- package/src/plugins/identity/concerns/resource-schemas.js +273 -0
- package/src/plugins/identity/concerns/token-generator.js +172 -0
- package/src/plugins/identity/email-service.js +422 -0
- package/src/plugins/identity/index.js +1052 -0
- package/src/plugins/identity/oauth2-server.js +1033 -0
- package/src/plugins/identity/oidc-discovery.js +285 -0
- package/src/plugins/identity/rsa-keys.js +323 -0
- package/src/plugins/identity/server.js +500 -0
- package/src/plugins/identity/session-manager.js +453 -0
- package/src/plugins/identity/ui/layouts/base.js +251 -0
- package/src/plugins/identity/ui/middleware.js +135 -0
- package/src/plugins/identity/ui/pages/admin/client-form.js +247 -0
- package/src/plugins/identity/ui/pages/admin/clients.js +179 -0
- package/src/plugins/identity/ui/pages/admin/dashboard.js +181 -0
- package/src/plugins/identity/ui/pages/admin/user-form.js +283 -0
- package/src/plugins/identity/ui/pages/admin/users.js +263 -0
- package/src/plugins/identity/ui/pages/consent.js +262 -0
- package/src/plugins/identity/ui/pages/forgot-password.js +104 -0
- package/src/plugins/identity/ui/pages/login.js +144 -0
- package/src/plugins/identity/ui/pages/mfa-backup-codes.js +180 -0
- package/src/plugins/identity/ui/pages/mfa-enrollment.js +187 -0
- package/src/plugins/identity/ui/pages/mfa-verification.js +178 -0
- package/src/plugins/identity/ui/pages/oauth-error.js +225 -0
- package/src/plugins/identity/ui/pages/profile.js +361 -0
- package/src/plugins/identity/ui/pages/register.js +226 -0
- package/src/plugins/identity/ui/pages/reset-password.js +128 -0
- package/src/plugins/identity/ui/pages/verify-email.js +172 -0
- package/src/plugins/identity/ui/routes.js +2541 -0
- package/src/plugins/identity/ui/styles/main.css +465 -0
- package/src/plugins/index.js +4 -1
- package/src/plugins/ml/base-model.class.js +65 -16
- package/src/plugins/ml/classification-model.class.js +1 -1
- package/src/plugins/ml/timeseries-model.class.js +3 -1
- package/src/plugins/ml.plugin.js +584 -31
- package/src/plugins/shared/error-handler.js +147 -0
- package/src/plugins/shared/index.js +9 -0
- package/src/plugins/shared/middlewares/compression.js +117 -0
- package/src/plugins/shared/middlewares/cors.js +49 -0
- package/src/plugins/shared/middlewares/index.js +11 -0
- package/src/plugins/shared/middlewares/logging.js +54 -0
- package/src/plugins/shared/middlewares/rate-limit.js +73 -0
- package/src/plugins/shared/middlewares/security.js +158 -0
- package/src/plugins/shared/response-formatter.js +264 -0
- package/src/plugins/state-machine.plugin.js +57 -2
- package/src/resource.class.js +140 -12
- package/src/schema.class.js +30 -1
- package/src/validator.class.js +57 -6
- package/dist/s3db.cjs.js.map +0 -1
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
import { BaseCloudDriver } from './base-driver.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Production-ready Hetzner Cloud inventory driver using hcloud-js library.
|
|
5
|
+
*
|
|
6
|
+
* Covers 12+ services with 15+ resource types:
|
|
7
|
+
* - Compute (servers/VPS, placement groups)
|
|
8
|
+
* - Storage (volumes)
|
|
9
|
+
* - Networking (networks, load balancers, firewalls, floating IPs, primary IPs)
|
|
10
|
+
* - SSH Keys, Images, Certificates, ISOs
|
|
11
|
+
*
|
|
12
|
+
* @see https://docs.hetzner.cloud/
|
|
13
|
+
* @see https://github.com/dennisbruner/hcloud-js
|
|
14
|
+
*/
|
|
15
|
+
export class HetznerInventoryDriver extends BaseCloudDriver {
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
super({ ...options, driver: options.driver || 'hetzner' });
|
|
18
|
+
|
|
19
|
+
this._apiToken = null;
|
|
20
|
+
this._client = null;
|
|
21
|
+
this._accountId = this.config?.accountId || 'hetzner';
|
|
22
|
+
|
|
23
|
+
// Services to collect (can be filtered via config.services)
|
|
24
|
+
this._services = this.config?.services || [
|
|
25
|
+
'servers',
|
|
26
|
+
'volumes',
|
|
27
|
+
'networks',
|
|
28
|
+
'loadbalancers',
|
|
29
|
+
'firewalls',
|
|
30
|
+
'floatingips',
|
|
31
|
+
'sshkeys',
|
|
32
|
+
'images',
|
|
33
|
+
'certificates',
|
|
34
|
+
'primaryips',
|
|
35
|
+
'placementgroups',
|
|
36
|
+
'isos'
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Initialize Hetzner Cloud API client.
|
|
42
|
+
*/
|
|
43
|
+
async _initializeClient() {
|
|
44
|
+
if (this._client) return;
|
|
45
|
+
|
|
46
|
+
const credentials = this.credentials || {};
|
|
47
|
+
this._apiToken = credentials.token || credentials.apiToken || process.env.HETZNER_TOKEN;
|
|
48
|
+
|
|
49
|
+
if (!this._apiToken) {
|
|
50
|
+
throw new Error('Hetzner API token is required. Provide via credentials.token or HETZNER_TOKEN env var.');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Lazy import to keep core package lightweight
|
|
54
|
+
const hcloud = await import('hcloud-js');
|
|
55
|
+
this._client = new hcloud.Client(this._apiToken);
|
|
56
|
+
|
|
57
|
+
this.logger('info', 'Hetzner Cloud API client initialized', {
|
|
58
|
+
accountId: this._accountId,
|
|
59
|
+
services: this._services.length
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Main entry point - lists all resources from configured services.
|
|
65
|
+
*/
|
|
66
|
+
async *listResources(options = {}) {
|
|
67
|
+
await this._initializeClient();
|
|
68
|
+
|
|
69
|
+
const serviceCollectors = {
|
|
70
|
+
servers: () => this._collectServers(),
|
|
71
|
+
volumes: () => this._collectVolumes(),
|
|
72
|
+
networks: () => this._collectNetworks(),
|
|
73
|
+
loadbalancers: () => this._collectLoadBalancers(),
|
|
74
|
+
firewalls: () => this._collectFirewalls(),
|
|
75
|
+
floatingips: () => this._collectFloatingIPs(),
|
|
76
|
+
sshkeys: () => this._collectSSHKeys(),
|
|
77
|
+
images: () => this._collectImages(),
|
|
78
|
+
certificates: () => this._collectCertificates(),
|
|
79
|
+
primaryips: () => this._collectPrimaryIPs(),
|
|
80
|
+
placementgroups: () => this._collectPlacementGroups(),
|
|
81
|
+
isos: () => this._collectISOs()
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
for (const service of this._services) {
|
|
85
|
+
const collector = serviceCollectors[service];
|
|
86
|
+
if (!collector) {
|
|
87
|
+
this.logger('warn', `Unknown Hetzner service: ${service}`, { service });
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
this.logger('info', `Collecting Hetzner ${service} resources`, { service });
|
|
93
|
+
yield* collector();
|
|
94
|
+
} catch (err) {
|
|
95
|
+
// Continue with next service instead of failing entire sync
|
|
96
|
+
this.logger('error', `Hetzner service collection failed, skipping to next service`, {
|
|
97
|
+
service,
|
|
98
|
+
error: err.message,
|
|
99
|
+
errorName: err.name,
|
|
100
|
+
stack: err.stack
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Collect Servers (VPS).
|
|
108
|
+
*/
|
|
109
|
+
async *_collectServers() {
|
|
110
|
+
try {
|
|
111
|
+
const response = await this._client.servers.list();
|
|
112
|
+
const servers = response.servers || [];
|
|
113
|
+
|
|
114
|
+
for (const server of servers) {
|
|
115
|
+
yield {
|
|
116
|
+
provider: 'hetzner',
|
|
117
|
+
accountId: this._accountId,
|
|
118
|
+
region: server.datacenter?.location?.name || null,
|
|
119
|
+
service: 'servers',
|
|
120
|
+
resourceType: 'hetzner.server',
|
|
121
|
+
resourceId: server.id?.toString(),
|
|
122
|
+
name: server.name,
|
|
123
|
+
tags: this._extractLabels(server.labels),
|
|
124
|
+
configuration: this._sanitize(server)
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this.logger('info', `Collected ${servers.length} Hetzner servers`);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
this.logger('error', 'Failed to collect Hetzner servers', {
|
|
131
|
+
error: err.message,
|
|
132
|
+
stack: err.stack
|
|
133
|
+
});
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Collect Volumes (block storage).
|
|
140
|
+
*/
|
|
141
|
+
async *_collectVolumes() {
|
|
142
|
+
try {
|
|
143
|
+
const response = await this._client.volumes.list();
|
|
144
|
+
const volumes = response.volumes || [];
|
|
145
|
+
|
|
146
|
+
for (const volume of volumes) {
|
|
147
|
+
yield {
|
|
148
|
+
provider: 'hetzner',
|
|
149
|
+
accountId: this._accountId,
|
|
150
|
+
region: volume.location?.name || null,
|
|
151
|
+
service: 'volumes',
|
|
152
|
+
resourceType: 'hetzner.volume',
|
|
153
|
+
resourceId: volume.id?.toString(),
|
|
154
|
+
name: volume.name,
|
|
155
|
+
tags: this._extractLabels(volume.labels),
|
|
156
|
+
configuration: this._sanitize(volume)
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this.logger('info', `Collected ${volumes.length} Hetzner volumes`);
|
|
161
|
+
} catch (err) {
|
|
162
|
+
this.logger('error', 'Failed to collect Hetzner volumes', {
|
|
163
|
+
error: err.message,
|
|
164
|
+
stack: err.stack
|
|
165
|
+
});
|
|
166
|
+
throw err;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Collect Networks (private networks/VPC).
|
|
172
|
+
*/
|
|
173
|
+
async *_collectNetworks() {
|
|
174
|
+
try {
|
|
175
|
+
const response = await this._client.networks.list();
|
|
176
|
+
const networks = response.networks || [];
|
|
177
|
+
|
|
178
|
+
for (const network of networks) {
|
|
179
|
+
yield {
|
|
180
|
+
provider: 'hetzner',
|
|
181
|
+
accountId: this._accountId,
|
|
182
|
+
region: null, // Networks span multiple locations
|
|
183
|
+
service: 'networks',
|
|
184
|
+
resourceType: 'hetzner.network',
|
|
185
|
+
resourceId: network.id?.toString(),
|
|
186
|
+
name: network.name,
|
|
187
|
+
tags: this._extractLabels(network.labels),
|
|
188
|
+
configuration: this._sanitize(network)
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Subnets are embedded in network
|
|
192
|
+
if (network.subnets && Array.isArray(network.subnets)) {
|
|
193
|
+
for (const subnet of network.subnets) {
|
|
194
|
+
yield {
|
|
195
|
+
provider: 'hetzner',
|
|
196
|
+
accountId: this._accountId,
|
|
197
|
+
region: subnet.network_zone,
|
|
198
|
+
service: 'networks',
|
|
199
|
+
resourceType: 'hetzner.network.subnet',
|
|
200
|
+
resourceId: `${network.id}/subnet/${subnet.ip_range}`,
|
|
201
|
+
name: `${network.name}-${subnet.type}`,
|
|
202
|
+
tags: {},
|
|
203
|
+
metadata: { networkId: network.id, networkName: network.name },
|
|
204
|
+
configuration: this._sanitize(subnet)
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
this.logger('info', `Collected ${networks.length} Hetzner networks`);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
this.logger('error', 'Failed to collect Hetzner networks', {
|
|
213
|
+
error: err.message,
|
|
214
|
+
stack: err.stack
|
|
215
|
+
});
|
|
216
|
+
throw err;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Collect Load Balancers.
|
|
222
|
+
*/
|
|
223
|
+
async *_collectLoadBalancers() {
|
|
224
|
+
try {
|
|
225
|
+
const response = await this._client.loadBalancers.list();
|
|
226
|
+
const loadBalancers = response.load_balancers || [];
|
|
227
|
+
|
|
228
|
+
for (const lb of loadBalancers) {
|
|
229
|
+
yield {
|
|
230
|
+
provider: 'hetzner',
|
|
231
|
+
accountId: this._accountId,
|
|
232
|
+
region: lb.location?.name || null,
|
|
233
|
+
service: 'loadbalancers',
|
|
234
|
+
resourceType: 'hetzner.loadbalancer',
|
|
235
|
+
resourceId: lb.id?.toString(),
|
|
236
|
+
name: lb.name,
|
|
237
|
+
tags: this._extractLabels(lb.labels),
|
|
238
|
+
configuration: this._sanitize(lb)
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
this.logger('info', `Collected ${loadBalancers.length} Hetzner load balancers`);
|
|
243
|
+
} catch (err) {
|
|
244
|
+
this.logger('error', 'Failed to collect Hetzner load balancers', {
|
|
245
|
+
error: err.message,
|
|
246
|
+
stack: err.stack
|
|
247
|
+
});
|
|
248
|
+
throw err;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Collect Firewalls.
|
|
254
|
+
*/
|
|
255
|
+
async *_collectFirewalls() {
|
|
256
|
+
try {
|
|
257
|
+
const response = await this._client.firewalls.list();
|
|
258
|
+
const firewalls = response.firewalls || [];
|
|
259
|
+
|
|
260
|
+
for (const firewall of firewalls) {
|
|
261
|
+
yield {
|
|
262
|
+
provider: 'hetzner',
|
|
263
|
+
accountId: this._accountId,
|
|
264
|
+
region: null, // Firewalls are global
|
|
265
|
+
service: 'firewalls',
|
|
266
|
+
resourceType: 'hetzner.firewall',
|
|
267
|
+
resourceId: firewall.id?.toString(),
|
|
268
|
+
name: firewall.name,
|
|
269
|
+
tags: this._extractLabels(firewall.labels),
|
|
270
|
+
configuration: this._sanitize(firewall)
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
this.logger('info', `Collected ${firewalls.length} Hetzner firewalls`);
|
|
275
|
+
} catch (err) {
|
|
276
|
+
this.logger('error', 'Failed to collect Hetzner firewalls', {
|
|
277
|
+
error: err.message,
|
|
278
|
+
stack: err.stack
|
|
279
|
+
});
|
|
280
|
+
throw err;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Collect Floating IPs.
|
|
286
|
+
*/
|
|
287
|
+
async *_collectFloatingIPs() {
|
|
288
|
+
try {
|
|
289
|
+
const response = await this._client.floatingIPs.list();
|
|
290
|
+
const floatingIPs = response.floating_ips || [];
|
|
291
|
+
|
|
292
|
+
for (const fip of floatingIPs) {
|
|
293
|
+
yield {
|
|
294
|
+
provider: 'hetzner',
|
|
295
|
+
accountId: this._accountId,
|
|
296
|
+
region: fip.home_location?.name || null,
|
|
297
|
+
service: 'floatingips',
|
|
298
|
+
resourceType: 'hetzner.floatingip',
|
|
299
|
+
resourceId: fip.id?.toString(),
|
|
300
|
+
name: fip.name || fip.ip,
|
|
301
|
+
tags: this._extractLabels(fip.labels),
|
|
302
|
+
configuration: this._sanitize(fip)
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
this.logger('info', `Collected ${floatingIPs.length} Hetzner floating IPs`);
|
|
307
|
+
} catch (err) {
|
|
308
|
+
this.logger('error', 'Failed to collect Hetzner floating IPs', {
|
|
309
|
+
error: err.message,
|
|
310
|
+
stack: err.stack
|
|
311
|
+
});
|
|
312
|
+
throw err;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Collect SSH Keys.
|
|
318
|
+
*/
|
|
319
|
+
async *_collectSSHKeys() {
|
|
320
|
+
try {
|
|
321
|
+
const response = await this._client.sshKeys.list();
|
|
322
|
+
const sshKeys = response.ssh_keys || [];
|
|
323
|
+
|
|
324
|
+
for (const key of sshKeys) {
|
|
325
|
+
yield {
|
|
326
|
+
provider: 'hetzner',
|
|
327
|
+
accountId: this._accountId,
|
|
328
|
+
region: null, // SSH keys are global
|
|
329
|
+
service: 'sshkeys',
|
|
330
|
+
resourceType: 'hetzner.sshkey',
|
|
331
|
+
resourceId: key.id?.toString(),
|
|
332
|
+
name: key.name,
|
|
333
|
+
tags: this._extractLabels(key.labels),
|
|
334
|
+
configuration: this._sanitize(key)
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
this.logger('info', `Collected ${sshKeys.length} Hetzner SSH keys`);
|
|
339
|
+
} catch (err) {
|
|
340
|
+
this.logger('error', 'Failed to collect Hetzner SSH keys', {
|
|
341
|
+
error: err.message,
|
|
342
|
+
stack: err.stack
|
|
343
|
+
});
|
|
344
|
+
throw err;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Collect custom Images.
|
|
350
|
+
*/
|
|
351
|
+
async *_collectImages() {
|
|
352
|
+
try {
|
|
353
|
+
const response = await this._client.images.list();
|
|
354
|
+
const images = response.images || [];
|
|
355
|
+
|
|
356
|
+
// Filter to only custom images (type = 'snapshot' or 'backup')
|
|
357
|
+
const customImages = images.filter(img => img.type === 'snapshot' || img.type === 'backup');
|
|
358
|
+
|
|
359
|
+
for (const image of customImages) {
|
|
360
|
+
yield {
|
|
361
|
+
provider: 'hetzner',
|
|
362
|
+
accountId: this._accountId,
|
|
363
|
+
region: null, // Images are global
|
|
364
|
+
service: 'images',
|
|
365
|
+
resourceType: 'hetzner.image',
|
|
366
|
+
resourceId: image.id?.toString(),
|
|
367
|
+
name: image.description || image.name || image.id?.toString(),
|
|
368
|
+
tags: this._extractLabels(image.labels),
|
|
369
|
+
configuration: this._sanitize(image)
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
this.logger('info', `Collected ${customImages.length} custom Hetzner images`);
|
|
374
|
+
} catch (err) {
|
|
375
|
+
this.logger('error', 'Failed to collect Hetzner images', {
|
|
376
|
+
error: err.message,
|
|
377
|
+
stack: err.stack
|
|
378
|
+
});
|
|
379
|
+
throw err;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Collect SSL Certificates.
|
|
385
|
+
*/
|
|
386
|
+
async *_collectCertificates() {
|
|
387
|
+
try {
|
|
388
|
+
const response = await this._client.certificates.list();
|
|
389
|
+
const certificates = response.certificates || [];
|
|
390
|
+
|
|
391
|
+
for (const cert of certificates) {
|
|
392
|
+
yield {
|
|
393
|
+
provider: 'hetzner',
|
|
394
|
+
accountId: this._accountId,
|
|
395
|
+
region: null, // Certificates are global
|
|
396
|
+
service: 'certificates',
|
|
397
|
+
resourceType: 'hetzner.certificate',
|
|
398
|
+
resourceId: cert.id?.toString(),
|
|
399
|
+
name: cert.name,
|
|
400
|
+
tags: this._extractLabels(cert.labels),
|
|
401
|
+
configuration: this._sanitize(cert)
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
this.logger('info', `Collected ${certificates.length} Hetzner certificates`);
|
|
406
|
+
} catch (err) {
|
|
407
|
+
this.logger('error', 'Failed to collect Hetzner certificates', {
|
|
408
|
+
error: err.message,
|
|
409
|
+
stack: err.stack
|
|
410
|
+
});
|
|
411
|
+
throw err;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Collect Primary IPs (independent public IPs).
|
|
417
|
+
*/
|
|
418
|
+
async *_collectPrimaryIPs() {
|
|
419
|
+
try {
|
|
420
|
+
const response = await this._client.primaryIPs.list();
|
|
421
|
+
const primaryIPs = response.primary_ips || [];
|
|
422
|
+
|
|
423
|
+
for (const ip of primaryIPs) {
|
|
424
|
+
yield {
|
|
425
|
+
provider: 'hetzner',
|
|
426
|
+
accountId: this._accountId,
|
|
427
|
+
region: ip.datacenter?.location?.name || null,
|
|
428
|
+
service: 'primaryips',
|
|
429
|
+
resourceType: 'hetzner.primaryip',
|
|
430
|
+
resourceId: ip.id?.toString(),
|
|
431
|
+
name: ip.name || ip.ip,
|
|
432
|
+
tags: this._extractLabels(ip.labels),
|
|
433
|
+
metadata: {
|
|
434
|
+
type: ip.type, // ipv4 or ipv6
|
|
435
|
+
assignedToId: ip.assignee_id
|
|
436
|
+
},
|
|
437
|
+
configuration: this._sanitize(ip)
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
this.logger('info', `Collected ${primaryIPs.length} Hetzner primary IPs`);
|
|
442
|
+
} catch (err) {
|
|
443
|
+
this.logger('error', 'Failed to collect Hetzner primary IPs', {
|
|
444
|
+
error: err.message,
|
|
445
|
+
stack: err.stack
|
|
446
|
+
});
|
|
447
|
+
throw err;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Collect Placement Groups (server anti-affinity).
|
|
453
|
+
*/
|
|
454
|
+
async *_collectPlacementGroups() {
|
|
455
|
+
try {
|
|
456
|
+
const response = await this._client.placementGroups.list();
|
|
457
|
+
const placementGroups = response.placement_groups || [];
|
|
458
|
+
|
|
459
|
+
for (const pg of placementGroups) {
|
|
460
|
+
yield {
|
|
461
|
+
provider: 'hetzner',
|
|
462
|
+
accountId: this._accountId,
|
|
463
|
+
region: null, // Placement groups are global
|
|
464
|
+
service: 'placementgroups',
|
|
465
|
+
resourceType: 'hetzner.placementgroup',
|
|
466
|
+
resourceId: pg.id?.toString(),
|
|
467
|
+
name: pg.name,
|
|
468
|
+
tags: this._extractLabels(pg.labels),
|
|
469
|
+
metadata: {
|
|
470
|
+
type: pg.type,
|
|
471
|
+
servers: pg.servers || []
|
|
472
|
+
},
|
|
473
|
+
configuration: this._sanitize(pg)
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
this.logger('info', `Collected ${placementGroups.length} Hetzner placement groups`);
|
|
478
|
+
} catch (err) {
|
|
479
|
+
this.logger('error', 'Failed to collect Hetzner placement groups', {
|
|
480
|
+
error: err.message,
|
|
481
|
+
stack: err.stack
|
|
482
|
+
});
|
|
483
|
+
throw err;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Collect ISOs (custom installation images).
|
|
489
|
+
*/
|
|
490
|
+
async *_collectISOs() {
|
|
491
|
+
try {
|
|
492
|
+
const response = await this._client.isos.list();
|
|
493
|
+
const isos = response.isos || [];
|
|
494
|
+
|
|
495
|
+
for (const iso of isos) {
|
|
496
|
+
yield {
|
|
497
|
+
provider: 'hetzner',
|
|
498
|
+
accountId: this._accountId,
|
|
499
|
+
region: null, // ISOs are global
|
|
500
|
+
service: 'isos',
|
|
501
|
+
resourceType: 'hetzner.iso',
|
|
502
|
+
resourceId: iso.id?.toString(),
|
|
503
|
+
name: iso.name,
|
|
504
|
+
tags: {},
|
|
505
|
+
metadata: {
|
|
506
|
+
type: iso.type,
|
|
507
|
+
deprecated: iso.deprecated,
|
|
508
|
+
deprecation: iso.deprecation
|
|
509
|
+
},
|
|
510
|
+
configuration: this._sanitize(iso)
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
this.logger('info', `Collected ${isos.length} Hetzner ISOs`);
|
|
515
|
+
} catch (err) {
|
|
516
|
+
this.logger('error', 'Failed to collect Hetzner ISOs', {
|
|
517
|
+
error: err.message,
|
|
518
|
+
stack: err.stack
|
|
519
|
+
});
|
|
520
|
+
throw err;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Extract labels from Hetzner labels object.
|
|
526
|
+
*/
|
|
527
|
+
_extractLabels(labels) {
|
|
528
|
+
if (!labels || typeof labels !== 'object') return {};
|
|
529
|
+
return { ...labels };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Sanitize configuration by removing sensitive data.
|
|
534
|
+
*/
|
|
535
|
+
_sanitize(config) {
|
|
536
|
+
if (!config || typeof config !== 'object') return config;
|
|
537
|
+
|
|
538
|
+
const sanitized = { ...config };
|
|
539
|
+
const sensitiveFields = [
|
|
540
|
+
'root_password',
|
|
541
|
+
'password',
|
|
542
|
+
'token',
|
|
543
|
+
'secret',
|
|
544
|
+
'api_key',
|
|
545
|
+
'private_key',
|
|
546
|
+
'public_key',
|
|
547
|
+
'certificate',
|
|
548
|
+
'private_key'
|
|
549
|
+
];
|
|
550
|
+
|
|
551
|
+
for (const field of sensitiveFields) {
|
|
552
|
+
if (field in sanitized) {
|
|
553
|
+
sanitized[field] = '***REDACTED***';
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return sanitized;
|
|
558
|
+
}
|
|
559
|
+
}
|