alchemy-hetzner-k3s 0.1.0-alpha.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/LICENSE ADDED
@@ -0,0 +1,14 @@
1
+ Copyright 2026 toolbar23
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ https://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
14
+
package/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # alchemy-hetzner-k3s
2
+
3
+ Production-oriented Hetzner K3s clusters that implement Alchemy's
4
+ `Kubernetes.ClusterLike` contract.
5
+
6
+ See the
7
+ [repository documentation](https://github.com/toolbar23/alchemy-k3s#hetzner-cluster)
8
+ for configuration, Secret encryption migration and recovery, encrypted state
9
+ requirements, Kubernetes Secret handling, upgrades, and deletion details.
@@ -0,0 +1,263 @@
1
+ import * as Hetzner from "alchemy/Hetzner";
2
+ import { Resource, Stage } from "alchemy";
3
+ import * as Effect from "effect/Effect";
4
+ import * as Provider from "alchemy/Provider";
5
+ import * as Redacted from "effect/Redacted";
6
+ import * as Layer from "effect/Layer";
7
+ import * as Kubernetes from "alchemy/Kubernetes";
8
+ import { S3BucketAccess } from "alchemy-s3-access";
9
+ //#region src/providers.d.ts
10
+ declare const Providers_base: Provider.ProviderCollection<Providers, "HetznerK3s">;
11
+ declare class Providers extends Providers_base {}
12
+ declare const providers: () => Layer.Layer<Providers, never, never>;
13
+ //#endregion
14
+ //#region ../shared/src/types.d.ts
15
+ type DayOfWeek = "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday" | "Sunday";
16
+ interface UpdateWindow {
17
+ /** Days on which an update may start. */
18
+ days: DayOfWeek[];
19
+ /** Inclusive start in 24-hour HH:mm form. */
20
+ startTime: `${number}:${number}`;
21
+ /** Exclusive end in 24-hour HH:mm form. */
22
+ endTime: `${number}:${number}`;
23
+ /** IANA time-zone name, for example Europe/Berlin. */
24
+ timeZone: string;
25
+ }
26
+ interface K3sDefinition {
27
+ /** A pinned Kubernetes minor channel such as v1.35. */
28
+ channel: `v1.${number}`;
29
+ /** Required maintenance window for automatic patch updates. */
30
+ updateWindow: UpdateWindow;
31
+ clusterCidr?: string;
32
+ serviceCidr?: string;
33
+ clusterDns?: string;
34
+ addons?: {
35
+ traefik?: boolean;
36
+ metricsServer?: boolean;
37
+ };
38
+ /** K3s' built-in Flannel data plane. @default "vxlan" */
39
+ flannelBackend?: "vxlan" | "wireguard-native";
40
+ }
41
+ interface NormalizedK3sDefinition {
42
+ channel: `v1.${number}`;
43
+ updateWindow: UpdateWindow;
44
+ clusterCidr: string;
45
+ serviceCidr: string;
46
+ clusterDns: string;
47
+ addons: {
48
+ traefik: boolean;
49
+ metricsServer: boolean;
50
+ };
51
+ flannelBackend: "vxlan" | "wireguard-native";
52
+ }
53
+ interface ClusterVersion {
54
+ node: string;
55
+ version: string;
56
+ }
57
+ //#endregion
58
+ //#region src/types.d.ts
59
+ interface WorkerPool {
60
+ name: string;
61
+ serverType: string;
62
+ location: string;
63
+ count: number;
64
+ /** Changing this value performs a create-first rolling replacement. */
65
+ replacementToken?: string;
66
+ labels?: Record<string, string>;
67
+ taints?: string[];
68
+ }
69
+ interface EtcdSnapshotConfig {
70
+ /** K3s cron expression. @default "0 * * * *" */
71
+ schedule?: string;
72
+ /** @default 24 */
73
+ retention?: number;
74
+ /** Prefix owned by this cluster inside the bucket. */
75
+ folder?: string;
76
+ /** The bucket is external and must outlive the cluster. */
77
+ s3?: S3BucketAccess;
78
+ }
79
+ type RecoveryFailurePoint = "after-server-creation" | "after-snapshot-selection" | "after-snapshot-download" | "after-etcd-reset" | "after-normal-start" | "after-state-persistence";
80
+ interface InitialControlPlaneRecovery {
81
+ /** Explicitly permits a replacement server to restore the old cluster. */
82
+ restoreOnInitialControlPlaneReplacement: true;
83
+ /** Reject snapshots older than this many seconds. */
84
+ maximumSnapshotAge: number;
85
+ /** Changing this value deliberately replaces only control plane 1. */
86
+ replacementToken?: string;
87
+ /** Test-only interruption; repeat the deploy without it to resume. */
88
+ failureInjection?: RecoveryFailurePoint;
89
+ }
90
+ interface ClusterProps {
91
+ k3s: K3sDefinition;
92
+ controlPlane: {
93
+ count: 1 | 3;
94
+ serverType: string;
95
+ /** One location shared by all servers, or exactly one per server. */
96
+ locations: string | string[];
97
+ };
98
+ workerPools: WorkerPool[];
99
+ state?: {
100
+ /** Assert provider-side encryption for an unrecognized remote state ID. */
101
+ encryptionAtRestConfirmed?: boolean;
102
+ };
103
+ ssh: {
104
+ allowedCidrs: string[];
105
+ /** Verify that this deploy runner is included in allowedCidrs. @default true */
106
+ validateCurrentIp?: boolean;
107
+ /** SSH uses the private network and public node ingress is closed. */
108
+ privateOnly?: boolean;
109
+ };
110
+ apiLoadBalancer?: {
111
+ type?: string;
112
+ location?: string;
113
+ };
114
+ networkCidr?: string;
115
+ /** @default false */
116
+ scheduleWorkloadsOnControlPlane?: boolean;
117
+ /** @default true */
118
+ protectAgainstDeletion?: boolean;
119
+ etcdSnapshots?: EtcdSnapshotConfig;
120
+ recovery?: InitialControlPlaneRecovery;
121
+ apiAuditLog?: {
122
+ /** @default true */
123
+ enabled?: boolean;
124
+ /** @default 30 */
125
+ maximumAgeDays?: number;
126
+ /** @default 10 */
127
+ maximumBackups?: number;
128
+ /** @default 100 */
129
+ maximumSizeMegabytes?: number;
130
+ };
131
+ secretsEncryption?: {
132
+ /**
133
+ * Required once to encrypt an existing cluster or migrate its provider
134
+ * from aescbc to secretbox. New clusters use encryption without this flag.
135
+ * @default false
136
+ */
137
+ migrateExisting?: boolean;
138
+ /** Controlled recovery testing; the next deploy resumes after removal. */
139
+ failureInjection?: SecretsEncryptionFailurePoint;
140
+ /** Change this opaque value to perform one dynamic key rotation. */
141
+ keyRotationToken?: string;
142
+ };
143
+ }
144
+ type SecretsEncryptionFailurePoint = "after-snapshot" | "after-enable" | "after-control-plane-restarts" | "after-rotate" | "after-final-restarts";
145
+ interface ServerReference {
146
+ id: number;
147
+ serverId: number;
148
+ name: string;
149
+ ipv4?: string;
150
+ privateKey?: Redacted.Redacted<string>;
151
+ /** Address used by the deploy runner (public or private). */
152
+ managementAddress?: string;
153
+ /** Pinned OpenSSH host public key created with the server. */
154
+ hostPublicKey?: string;
155
+ }
156
+ interface NodeReference {
157
+ logicalName: string;
158
+ name: string;
159
+ role: "server" | "agent";
160
+ serverId: number;
161
+ privateIp: string;
162
+ version: string;
163
+ token?: Redacted.Redacted<string>;
164
+ /** Kubernetes kube-system UID, persisted for S3 snapshot identity checks. */
165
+ clusterId?: string;
166
+ /** Old node object retained until cluster-wide recovery is healthy. */
167
+ obsoleteNodeName?: string;
168
+ recovery?: {
169
+ restoredSnapshot: string;
170
+ snapshotCreatedAt: string;
171
+ completedAt: string;
172
+ };
173
+ server: ServerReference;
174
+ }
175
+ interface NodeProps {
176
+ name: string;
177
+ role: "server" | "agent";
178
+ initialServer: boolean;
179
+ /** Internal revision that re-runs bootstrap when node configuration changes. */
180
+ bootstrapRevision: number;
181
+ server: ServerReference;
182
+ bootstrap?: NodeReference;
183
+ k3s: NormalizedK3sDefinition;
184
+ networkCidr: string;
185
+ apiEndpoint: string;
186
+ scheduleWorkloadsOnControlPlane: boolean;
187
+ labels?: Record<string, string>;
188
+ taints?: string[];
189
+ etcdSnapshots: {
190
+ schedule: string;
191
+ retention: number;
192
+ folder?: string;
193
+ s3?: S3BucketAccess;
194
+ };
195
+ hcloudToken: Redacted.Redacted<string>;
196
+ privateManagement: boolean;
197
+ stateId: string;
198
+ recovery?: InitialControlPlaneRecovery;
199
+ apiAuditLog: {
200
+ enabled: boolean;
201
+ maximumAgeDays: number;
202
+ maximumBackups: number;
203
+ maximumSizeMegabytes: number;
204
+ };
205
+ secretsEncryption?: {
206
+ migrateExisting: boolean;
207
+ failureInjection?: SecretsEncryptionFailurePoint;
208
+ };
209
+ }
210
+ type NodeResource = Resource<"Hetzner.K3s.Node", NodeProps, NodeReference, never, Providers>;
211
+ interface ClusterStateProps {
212
+ k3s: NormalizedK3sDefinition;
213
+ /** Pure dependency edges that hold cluster readiness behind every node. */
214
+ nodeServerIds: number[];
215
+ nodeNames: string[];
216
+ controlPlanes: NodeReference[];
217
+ loadBalancer: {
218
+ ipv4: string | null;
219
+ };
220
+ hcloudToken: Redacted.Redacted<string>;
221
+ networkName: string;
222
+ networkZone: string;
223
+ protectAgainstDeletion: boolean;
224
+ topologyFingerprint: string;
225
+ secretsEncryption: {
226
+ failureInjection?: SecretsEncryptionFailurePoint;
227
+ keyRotationToken?: string;
228
+ };
229
+ obsoleteNodeNames: string[];
230
+ }
231
+ interface ClusterAttributes {
232
+ connection: Kubernetes.Connection;
233
+ endpoint: string;
234
+ kubeconfigPath: string;
235
+ currentVersions: ClusterVersion[];
236
+ channel: `v1.${number}`;
237
+ topologyFingerprint: string;
238
+ secretsEncryption: {
239
+ enabled: boolean;
240
+ provider: "secretbox" | "aescbc" | undefined;
241
+ stage: string | undefined;
242
+ hashesMatch: boolean;
243
+ };
244
+ recovery?: {
245
+ restoredSnapshot: string;
246
+ snapshotCreatedAt: string;
247
+ completedAt: string;
248
+ };
249
+ }
250
+ type ClusterResource = Resource<"Hetzner.K3s.Cluster", ClusterStateProps, ClusterAttributes, never, Providers>;
251
+ /** Public return shape. It is directly accepted by Kubernetes resource inputs. */
252
+ type Cluster$1 = ClusterResource;
253
+ //#endregion
254
+ //#region src/cluster.d.ts
255
+ declare const Cluster: (id: string, props: ClusterProps) => Effect.Effect<ClusterResource, never, Providers | Hetzner.Providers | Stage>;
256
+ //#endregion
257
+ //#region src/cluster-state.d.ts
258
+ declare const ClusterState: import("alchemy").ResourceClass<ClusterResource>;
259
+ //#endregion
260
+ //#region src/node.d.ts
261
+ declare const Node: import("alchemy").ResourceClass<NodeResource>;
262
+ //#endregion
263
+ export { Cluster, type ClusterAttributes, type Cluster$1 as ClusterInstance, type ClusterProps, ClusterState, type EtcdSnapshotConfig, type InitialControlPlaneRecovery, type K3sDefinition, Node, type NodeReference, Providers, type RecoveryFailurePoint, type WorkerPool, providers };