@puls-dev/hcloud 0.5.3

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 ADDED
@@ -0,0 +1,55 @@
1
+ # @puls-dev/hcloud
2
+
3
+ **Hetzner Cloud (HCloud) Provider for Puls IaC. Declare servers, networks, firewalls, and SSH keys in strongly-typed TypeScript.**
4
+
5
+ ---
6
+
7
+ ## What is @puls-dev/hcloud?
8
+
9
+ This package is the official Hetzner Cloud provider plug-in for Puls. It manages HCloud resources securely by resolving live states directly through the Hetzner Cloud REST APIs.
10
+
11
+ ## Available Builders
12
+
13
+ * **\`HCloud.Server\`**: Provision cloud servers (analogous to Droplets) with support for SSH keys, server types, locations, and our signature Ansible playbook provisioning.
14
+ * **\`HCloud.SSHKey\`**: Register SSH public keys on your Hetzner Cloud account.
15
+ * **\`HCloud.Network\`**: Create private networks (VPCs) with custom IP ranges.
16
+ * **\`HCloud.Firewall\`**: Define inbound/outbound firewall rules.
17
+
18
+ ## Installation
19
+
20
+ \`\`\`bash
21
+ npm install @puls-dev/core @puls-dev/hcloud
22
+ \`\`\`
23
+
24
+ ## Quick Example
25
+
26
+ \`\`\`typescript
27
+ import { Stack, Deploy } from "@puls-dev/core";
28
+ import { HCloud, SERVER_TYPE, LOCATION, OS_IMAGE } from "@puls-dev/hcloud";
29
+
30
+ @Deploy()
31
+ class MyStack extends Stack {
32
+ key = HCloud.SSHKey("my-key")
33
+ .publicKey("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL...");
34
+
35
+ net = HCloud.Network("private-net")
36
+ .ipRange("10.0.0.0/16");
37
+
38
+ web = HCloud.Server("web-server")
39
+ .serverType(SERVER_TYPE.CX22)
40
+ .image(OS_IMAGE.UBUNTU_24_04)
41
+ .location(LOCATION.NBG1)
42
+ .sshKeys([this.key])
43
+ .networks([this.net])
44
+ .provision(["playbooks/web.yml"]);
45
+ }
46
+ \`\`\`
47
+
48
+ ## Authentication
49
+
50
+ Declare your Hetzner Cloud API token in your \`.env\` file:
51
+ \`\`\`bash
52
+ HCLOUD_TOKEN=your-hetzner-cloud-api-token
53
+ \`\`\`
54
+
55
+ Learn more at **[pulsdev.io/providers/hcloud](https://pulsdev.io/providers/hcloud.md)**.
package/dist/api.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ export declare class HCloudApiClient {
2
+ private token;
3
+ private static readonly BASE;
4
+ constructor(token: string);
5
+ private get authHeaders();
6
+ private createOfflineMock;
7
+ private request;
8
+ get<T>(path: string): Promise<T>;
9
+ post<T>(path: string, body: unknown): Promise<T>;
10
+ put<T>(path: string, body: unknown): Promise<T>;
11
+ delete(path: string, body?: unknown): Promise<void>;
12
+ waitForAction(actionId: number): Promise<void>;
13
+ }
14
+ export declare function getHCloudApi(): HCloudApiClient;
package/dist/api.js ADDED
@@ -0,0 +1,211 @@
1
+ import { Config } from '@puls-dev/core';
2
+ import { withRetry } from '@puls-dev/core';
3
+ import { resourceContextStorage } from '@puls-dev/core';
4
+ export class HCloudApiClient {
5
+ token;
6
+ static BASE = 'https://api.hetzner.cloud/v1';
7
+ constructor(token) {
8
+ this.token = token;
9
+ }
10
+ get authHeaders() {
11
+ return {
12
+ Authorization: `Bearer ${this.token}`,
13
+ 'Content-Type': 'application/json',
14
+ 'Accept-Encoding': 'identity'
15
+ };
16
+ }
17
+ createOfflineMock(method, path, body) {
18
+ if (path.includes("/servers")) {
19
+ const name = body?.name ?? "mock-server";
20
+ return {
21
+ server: {
22
+ id: 123456,
23
+ name,
24
+ status: "running",
25
+ public_net: {
26
+ ipv4: { ip: "159.69.1.2" }
27
+ },
28
+ private_net: [],
29
+ server_type: { name: body?.server_type ?? "cx22" },
30
+ image: { name: body?.image ?? "ubuntu-24.04" },
31
+ datacenter: { location: { name: body?.location ?? "nbg1" } }
32
+ },
33
+ servers: [
34
+ {
35
+ id: 123456,
36
+ name,
37
+ status: "running",
38
+ public_net: {
39
+ ipv4: { ip: "159.69.1.2" }
40
+ },
41
+ private_net: [],
42
+ server_type: { name: "cx22" },
43
+ image: { name: "ubuntu-24.04" },
44
+ datacenter: { location: { name: "nbg1" } }
45
+ }
46
+ ],
47
+ action: { id: 7890, status: "success" }
48
+ };
49
+ }
50
+ if (path.includes("/ssh_keys")) {
51
+ return {
52
+ ssh_key: {
53
+ id: 12345,
54
+ name: body?.name ?? "mock-key",
55
+ public_key: body?.public_key ?? "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL...",
56
+ fingerprint: "00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff"
57
+ },
58
+ ssh_keys: []
59
+ };
60
+ }
61
+ if (path.includes("/networks")) {
62
+ return {
63
+ network: {
64
+ id: 5678,
65
+ name: body?.name ?? "mock-net",
66
+ ip_range: body?.ip_range ?? "10.0.0.0/16"
67
+ },
68
+ networks: []
69
+ };
70
+ }
71
+ if (path.includes("/firewalls")) {
72
+ return {
73
+ firewall: {
74
+ id: 9012,
75
+ name: body?.name ?? "mock-fw",
76
+ rules: body?.rules ?? []
77
+ },
78
+ firewalls: []
79
+ };
80
+ }
81
+ if (path.includes("/actions/")) {
82
+ return { action: { id: 7890, status: "success" } };
83
+ }
84
+ return new Proxy({}, {
85
+ get(target, prop) {
86
+ if (prop === "then")
87
+ return undefined;
88
+ if (prop === "id")
89
+ return 123456;
90
+ if (prop === "name")
91
+ return "mock-hcloud-name";
92
+ if (prop === "status")
93
+ return "success";
94
+ if (prop.endsWith("s"))
95
+ return [];
96
+ return `mock-hcloud-${prop.toLowerCase()}`;
97
+ }
98
+ });
99
+ }
100
+ async request(fn) {
101
+ return withRetry(fn, {
102
+ retryable: (err) => {
103
+ const match = err.message.match(/: (\d+)/);
104
+ const status = match ? parseInt(match[1], 10) : null;
105
+ return status === 429 || (status && status >= 500) || err.message.includes("ETIMEDOUT");
106
+ }
107
+ });
108
+ }
109
+ async get(path) {
110
+ const context = resourceContextStorage.getStore();
111
+ const abortSignal = context?.abortSignal;
112
+ if (Config.isOfflineMode() || (Config.isGlobalDryRun() && this.token === "mock-hcloud-token")) {
113
+ return Promise.resolve(this.createOfflineMock('GET', path));
114
+ }
115
+ return this.request(async () => {
116
+ const res = await fetch(`${HCloudApiClient.BASE}${path}`, {
117
+ headers: this.authHeaders,
118
+ ...(abortSignal && { signal: abortSignal })
119
+ });
120
+ if (!res.ok)
121
+ throw new Error(`HCloud API GET ${path}: ${res.status} ${await res.text()}`);
122
+ return res.json();
123
+ });
124
+ }
125
+ async post(path, body) {
126
+ const context = resourceContextStorage.getStore();
127
+ const abortSignal = context?.abortSignal;
128
+ if (Config.isOfflineMode() || Config.isGlobalDryRun()) {
129
+ return Promise.resolve(this.createOfflineMock('POST', path, body));
130
+ }
131
+ return this.request(async () => {
132
+ const res = await fetch(`${HCloudApiClient.BASE}${path}`, {
133
+ method: 'POST',
134
+ headers: this.authHeaders,
135
+ body: JSON.stringify(body),
136
+ ...(abortSignal && { signal: abortSignal })
137
+ });
138
+ if (!res.ok)
139
+ throw new Error(`HCloud API POST ${path}: ${res.status} ${await res.text()}`);
140
+ return res.json();
141
+ });
142
+ }
143
+ async put(path, body) {
144
+ const context = resourceContextStorage.getStore();
145
+ const abortSignal = context?.abortSignal;
146
+ if (Config.isOfflineMode() || Config.isGlobalDryRun()) {
147
+ return Promise.resolve(this.createOfflineMock('PUT', path, body));
148
+ }
149
+ return this.request(async () => {
150
+ const res = await fetch(`${HCloudApiClient.BASE}${path}`, {
151
+ method: 'PUT',
152
+ headers: this.authHeaders,
153
+ body: JSON.stringify(body),
154
+ ...(abortSignal && { signal: abortSignal })
155
+ });
156
+ if (!res.ok)
157
+ throw new Error(`HCloud API PUT ${path}: ${res.status} ${await res.text()}`);
158
+ return res.json();
159
+ });
160
+ }
161
+ async delete(path, body) {
162
+ const context = resourceContextStorage.getStore();
163
+ const abortSignal = context?.abortSignal;
164
+ if (Config.isOfflineMode() || Config.isGlobalDryRun()) {
165
+ return Promise.resolve();
166
+ }
167
+ return this.request(async () => {
168
+ const res = await fetch(`${HCloudApiClient.BASE}${path}`, {
169
+ method: 'DELETE',
170
+ headers: this.authHeaders,
171
+ ...(body !== undefined && { body: JSON.stringify(body) }),
172
+ ...(abortSignal && { signal: abortSignal })
173
+ });
174
+ if (!res.ok && res.status !== 404) {
175
+ throw new Error(`HCloud API DELETE ${path}: ${res.status} ${await res.text()}`);
176
+ }
177
+ });
178
+ }
179
+ async waitForAction(actionId) {
180
+ if (Config.isOfflineMode() || Config.isGlobalDryRun()) {
181
+ return Promise.resolve();
182
+ }
183
+ const context = resourceContextStorage.getStore();
184
+ const abortSignal = context?.abortSignal;
185
+ while (true) {
186
+ if (abortSignal?.aborted) {
187
+ throw new Error("Action wait aborted");
188
+ }
189
+ const res = await this.get(`/actions/${actionId}`);
190
+ if (res.action.status === 'success') {
191
+ return;
192
+ }
193
+ if (res.action.status === 'error') {
194
+ throw new Error(`HCloud Action ${actionId} failed: ${res.action.error?.message}`);
195
+ }
196
+ // Wait 1 second before polling again
197
+ await new Promise((resolve) => setTimeout(resolve, 1000));
198
+ }
199
+ }
200
+ }
201
+ let apiInstance = null;
202
+ export function getHCloudApi() {
203
+ if (apiInstance)
204
+ return apiInstance;
205
+ const token = process.env.HCLOUD_TOKEN || Config.get().providers.hcloud?.token;
206
+ if (!token && !Config.isOfflineMode()) {
207
+ throw new Error("Hetzner Cloud token not configured. Set HCLOUD_TOKEN environment variable.");
208
+ }
209
+ apiInstance = new HCloudApiClient(token || "mock-hcloud-token");
210
+ return apiInstance;
211
+ }
@@ -0,0 +1,27 @@
1
+ import { BaseBuilder } from '@puls-dev/core';
2
+ export interface HCloudFirewallRule {
3
+ type: 'ingress' | 'egress';
4
+ protocol: 'tcp' | 'udp' | 'icmp' | 'esp' | 'gre';
5
+ port: number | string;
6
+ sources?: string[];
7
+ destinations?: string[];
8
+ }
9
+ export declare class FirewallBuilder extends BaseBuilder {
10
+ private _rules;
11
+ private serverNames;
12
+ private firewallId?;
13
+ private log;
14
+ constructor(name: string);
15
+ private discoverFirewall;
16
+ ingress(protocol: 'tcp' | 'udp' | 'icmp' | 'esp' | 'gre', port: number | string, sources: string[]): this;
17
+ egress(protocol: 'tcp' | 'udp' | 'icmp' | 'esp' | 'gre', port: number | string, destinations: string[]): this;
18
+ rules(filePath: string): this;
19
+ attachTo(serverName: string): this;
20
+ private resolveServerIds;
21
+ private buildApiRules;
22
+ deploy(): Promise<{
23
+ name: string;
24
+ rules: HCloudFirewallRule[];
25
+ }>;
26
+ destroy(): Promise<void>;
27
+ }
@@ -0,0 +1,133 @@
1
+ import { BaseBuilder } from '@puls-dev/core';
2
+ import { getHCloudApi } from './api.js';
3
+ import { loadRecordsFromFile } from '@puls-dev/core';
4
+ export class FirewallBuilder extends BaseBuilder {
5
+ _rules = [];
6
+ serverNames = [];
7
+ firewallId;
8
+ log(msg) {
9
+ console.log(` 🛡️ [HCloud.Firewall] ${msg}`);
10
+ }
11
+ constructor(name) {
12
+ super(name);
13
+ this.discoveryPromise = this.discoverFirewall(name);
14
+ }
15
+ async discoverFirewall(name) {
16
+ const api = getHCloudApi();
17
+ const data = await api.get('/firewalls');
18
+ const match = data.firewalls.find(f => f.name === name) ?? null;
19
+ if (match) {
20
+ this.firewallId = match.id;
21
+ }
22
+ return match;
23
+ }
24
+ ingress(protocol, port, sources) {
25
+ this._rules.push({ type: 'ingress', protocol, port, sources });
26
+ return this;
27
+ }
28
+ egress(protocol, port, destinations) {
29
+ this._rules.push({ type: 'egress', protocol, port, destinations });
30
+ return this;
31
+ }
32
+ rules(filePath) {
33
+ const loaded = loadRecordsFromFile(filePath);
34
+ for (const r of loaded) {
35
+ this._rules.push({
36
+ type: r.type,
37
+ protocol: r.protocol,
38
+ port: r.port,
39
+ sources: r.sources,
40
+ destinations: r.destinations,
41
+ });
42
+ }
43
+ return this;
44
+ }
45
+ attachTo(serverName) {
46
+ this.serverNames.push(serverName);
47
+ return this;
48
+ }
49
+ async resolveServerIds(api) {
50
+ const ids = [];
51
+ for (const name of this.serverNames) {
52
+ const data = await api.get(`/servers?name=${encodeURIComponent(name)}`);
53
+ const match = data.servers.find(s => s.name === name);
54
+ if (match)
55
+ ids.push(match.id);
56
+ }
57
+ return ids;
58
+ }
59
+ buildApiRules() {
60
+ return this._rules.map(r => {
61
+ const direction = r.type === 'ingress' ? 'in' : 'out';
62
+ const rule = {
63
+ direction,
64
+ protocol: r.protocol,
65
+ port: String(r.port),
66
+ };
67
+ if (direction === 'in') {
68
+ rule.source_ips = r.sources ?? [];
69
+ }
70
+ else {
71
+ rule.destination_ips = r.destinations ?? [];
72
+ }
73
+ return rule;
74
+ });
75
+ }
76
+ async deploy() {
77
+ const dryRun = this.isDryRunActive();
78
+ const existing = await this.discoveryPromise;
79
+ const api = getHCloudApi();
80
+ this.log(`Finalizing firewall...`);
81
+ if (dryRun) {
82
+ this._rules.forEach(r => {
83
+ const dir = r.type === 'ingress' ? 'from' : 'to';
84
+ const targets = r.type === 'ingress' ? r.sources : r.destinations;
85
+ this.log(`[PLAN] ${r.type.toUpperCase()}: ${r.protocol.toUpperCase()} ${r.port} ${dir} [${targets?.join(', ')}]`);
86
+ });
87
+ return { name: this.name, rules: this._rules };
88
+ }
89
+ const serverIds = await this.resolveServerIds(api);
90
+ const rules = this.buildApiRules();
91
+ const applyTo = serverIds.map(id => ({
92
+ type: 'server',
93
+ server: { id }
94
+ }));
95
+ if (existing) {
96
+ this.firewallId = existing.id;
97
+ // Update rules
98
+ await api.post(`/firewalls/${this.firewallId}/actions/set_rules`, { rules });
99
+ // Apply to resources
100
+ if (applyTo.length > 0) {
101
+ await api.post(`/firewalls/${this.firewallId}/actions/apply_to_resources`, { apply_to: applyTo });
102
+ }
103
+ this.log(`Updated firewall (id=${this.firewallId})`);
104
+ }
105
+ else {
106
+ const result = await api.post('/firewalls', {
107
+ name: this.name,
108
+ rules,
109
+ apply_to: applyTo
110
+ });
111
+ this.firewallId = result.firewall.id;
112
+ this.log(`Created firewall (id=${this.firewallId})`);
113
+ }
114
+ this._rules.forEach(r => {
115
+ const dir = r.type === 'ingress' ? 'from' : 'to';
116
+ const targets = r.type === 'ingress' ? r.sources : r.destinations;
117
+ this.log(`✅ ${r.type.toUpperCase()}: ${r.protocol.toUpperCase()} ${r.port} ${dir} [${targets?.join(', ')}]`);
118
+ });
119
+ return { name: this.name, rules: this._rules };
120
+ }
121
+ async destroy() {
122
+ const existing = await this.discoveryPromise;
123
+ if (existing) {
124
+ this.log(`Deleting firewall...`);
125
+ const api = getHCloudApi();
126
+ await api.delete(`/firewalls/${existing.id}`);
127
+ this.log(`Deleted firewall.`);
128
+ }
129
+ else {
130
+ this.log(`Firewall does not exist. Skipping deletion.`);
131
+ }
132
+ }
133
+ }
@@ -0,0 +1,23 @@
1
+ import "./plugin.js";
2
+ import { ServerBuilder } from "./server.js";
3
+ import { SSHKeyBuilder } from "./ssh_key.js";
4
+ import { NetworkBuilder } from "./network.js";
5
+ import { FirewallBuilder } from "./firewall.js";
6
+ export declare const HCloud: {
7
+ init: (opts: {
8
+ token: string;
9
+ defaultLocation?: string;
10
+ sshUser?: string;
11
+ }) => void;
12
+ Server: (name: string) => ServerBuilder;
13
+ SSHKey: (name: string) => SSHKeyBuilder;
14
+ Network: (name: string) => NetworkBuilder;
15
+ Firewall: (name: string) => FirewallBuilder;
16
+ };
17
+ export * from "./types/hcloud.js";
18
+ export { ServerBuilder } from "./server.js";
19
+ export { SSHKeyBuilder } from "./ssh_key.js";
20
+ export { NetworkBuilder } from "./network.js";
21
+ export { FirewallBuilder } from "./firewall.js";
22
+ export { HCloudApiClient, getHCloudApi } from "./api.js";
23
+ export { listHCloudResources } from "./list.js";
package/dist/index.js ADDED
@@ -0,0 +1,27 @@
1
+ import "./plugin.js";
2
+ import { Config } from "@puls-dev/core";
3
+ import { ServerBuilder } from "./server.js";
4
+ import { SSHKeyBuilder } from "./ssh_key.js";
5
+ import { NetworkBuilder } from "./network.js";
6
+ import { FirewallBuilder } from "./firewall.js";
7
+ export const HCloud = {
8
+ init: (opts) => {
9
+ Config.set({
10
+ providers: {
11
+ ...Config.get().providers,
12
+ hcloud: opts,
13
+ },
14
+ });
15
+ },
16
+ Server: (name) => new ServerBuilder(name),
17
+ SSHKey: (name) => new SSHKeyBuilder(name),
18
+ Network: (name) => new NetworkBuilder(name),
19
+ Firewall: (name) => new FirewallBuilder(name),
20
+ };
21
+ export * from "./types/hcloud.js";
22
+ export { ServerBuilder } from "./server.js";
23
+ export { SSHKeyBuilder } from "./ssh_key.js";
24
+ export { NetworkBuilder } from "./network.js";
25
+ export { FirewallBuilder } from "./firewall.js";
26
+ export { HCloudApiClient, getHCloudApi } from "./api.js";
27
+ export { listHCloudResources } from "./list.js";
package/dist/list.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import type { HCloudInventory } from '@puls-dev/core';
2
+ export declare function listHCloudResources(): Promise<HCloudInventory>;
package/dist/list.js ADDED
@@ -0,0 +1,45 @@
1
+ import { getHCloudApi } from './api.js';
2
+ const HCLOUD_PRICING = {
3
+ 'cx22': 3.60,
4
+ 'cpx11': 4.80,
5
+ 'cpx21': 7.90,
6
+ 'cpx31': 14.80,
7
+ 'cpx41': 29.60,
8
+ 'cpx51': 64.20,
9
+ 'cax11': 4.00,
10
+ 'cax21': 8.00,
11
+ 'cax31': 16.00,
12
+ 'cax41': 32.00,
13
+ };
14
+ function priceForType(type) {
15
+ return HCLOUD_PRICING[type] ?? 0;
16
+ }
17
+ export async function listHCloudResources() {
18
+ const api = getHCloudApi();
19
+ const [serversData, firewallsData, networksData] = await Promise.all([
20
+ api.get('/servers'),
21
+ api.get('/firewalls'),
22
+ api.get('/networks'),
23
+ ]);
24
+ const servers = serversData.servers.map((s) => ({
25
+ id: s.id,
26
+ name: s.name,
27
+ status: s.status,
28
+ location: s.datacenter?.location?.name ?? '',
29
+ serverType: s.server_type?.name ?? '',
30
+ ip: s.public_net?.ipv4?.ip,
31
+ monthlyCost: priceForType(s.server_type?.name),
32
+ }));
33
+ const firewalls = firewallsData.firewalls.map((f) => ({
34
+ id: f.id,
35
+ name: f.name,
36
+ serverCount: (f.applied_to ?? []).filter((r) => r.type === 'server').length,
37
+ }));
38
+ const networks = networksData.networks.map((n) => ({
39
+ id: n.id,
40
+ name: n.name,
41
+ ipRange: n.ip_range ?? '',
42
+ }));
43
+ const totalMonthlyCost = servers.reduce((sum, s) => sum + s.monthlyCost, 0);
44
+ return { servers, firewalls, networks, totalMonthlyCost };
45
+ }
@@ -0,0 +1,20 @@
1
+ import { BaseBuilder } from '@puls-dev/core';
2
+ import { Output } from '@puls-dev/core';
3
+ export declare class NetworkBuilder extends BaseBuilder {
4
+ readonly out: {
5
+ id: Output<number>;
6
+ };
7
+ private _ipRange;
8
+ private networkId?;
9
+ private log;
10
+ constructor(name: string);
11
+ private discoverNetwork;
12
+ ipRange(range: string): this;
13
+ getDiff(existing: any): {
14
+ field: string;
15
+ declared: string;
16
+ live: any;
17
+ }[];
18
+ deploy(): Promise<void>;
19
+ destroy(): Promise<void>;
20
+ }
@@ -0,0 +1,80 @@
1
+ import { BaseBuilder } from '@puls-dev/core';
2
+ import { Output } from '@puls-dev/core';
3
+ import { getHCloudApi } from './api.js';
4
+ export class NetworkBuilder extends BaseBuilder {
5
+ out = {
6
+ id: new Output(),
7
+ };
8
+ _ipRange = "10.0.0.0/16";
9
+ networkId;
10
+ log(msg) {
11
+ console.log(` 🌐 [HCloud.Network] ${msg}`);
12
+ }
13
+ constructor(name) {
14
+ super(name);
15
+ this.discoveryPromise = this.discoverNetwork(name);
16
+ }
17
+ async discoverNetwork(name) {
18
+ const api = getHCloudApi();
19
+ const data = await api.get('/networks');
20
+ const match = data.networks.find(n => n.name === name) ?? null;
21
+ if (match) {
22
+ this.networkId = match.id;
23
+ this.out.id.resolve(match.id);
24
+ }
25
+ return match;
26
+ }
27
+ ipRange(range) {
28
+ this._ipRange = range;
29
+ return this;
30
+ }
31
+ getDiff(existing) {
32
+ const diffs = [];
33
+ if (existing && existing.ip_range !== this._ipRange) {
34
+ diffs.push({ field: "ipRange", declared: this._ipRange, live: existing.ip_range });
35
+ }
36
+ return diffs;
37
+ }
38
+ async deploy() {
39
+ const api = getHCloudApi();
40
+ const existing = await this.discoveryPromise;
41
+ if (!existing) {
42
+ this.log(`Creating Network...`);
43
+ const res = await api.post('/networks', {
44
+ name: this.name,
45
+ ip_range: this._ipRange,
46
+ });
47
+ this.networkId = res.network.id;
48
+ this.out.id.resolve(res.network.id);
49
+ this.log(`Created Network with ID ${this.networkId}`);
50
+ }
51
+ else {
52
+ const diffs = this.getDiff(existing);
53
+ if (diffs.length > 0) {
54
+ this.log(`Network IP range has drifted. Re-creating...`);
55
+ await api.delete(`/networks/${this.networkId}`);
56
+ const res = await api.post('/networks', {
57
+ name: this.name,
58
+ ip_range: this._ipRange,
59
+ });
60
+ this.networkId = res.network.id;
61
+ this.out.id.resolve(res.network.id);
62
+ }
63
+ else {
64
+ this.log(`Network already exists and is up to date.`);
65
+ }
66
+ }
67
+ }
68
+ async destroy() {
69
+ const existing = await this.discoveryPromise;
70
+ if (existing) {
71
+ this.log(`Deleting Network...`);
72
+ const api = getHCloudApi();
73
+ await api.delete(`/networks/${this.networkId}`);
74
+ this.log(`Deleted Network.`);
75
+ }
76
+ else {
77
+ this.log(`Network does not exist. Skipping deletion.`);
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,9 @@
1
+ import { listHCloudResources } from "./list.js";
2
+ import type { HCloudInventory } from "@puls-dev/core";
3
+ export declare const hcloudPlugin: {
4
+ name: string;
5
+ isConfigured: (cfg: any) => boolean;
6
+ list: typeof listHCloudResources;
7
+ render: (inv: HCloudInventory) => void;
8
+ configure: (pOpts: any) => void;
9
+ };