pangea-kubernetes 0.1.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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/auto-bump.yml +11 -0
  3. data/.github/workflows/ci.yml +7 -0
  4. data/.github/workflows/release.yml +22 -0
  5. data/.gitignore +6 -0
  6. data/.rspec +3 -0
  7. data/AGENTS.md +3 -0
  8. data/CLAUDE.md +370 -0
  9. data/Gemfile +9 -0
  10. data/Gemfile.lock +128 -0
  11. data/README.md +42 -0
  12. data/Rakefile +8 -0
  13. data/flake.lock +2144 -0
  14. data/flake.nix +30 -0
  15. data/gemset.nix +312 -0
  16. data/lib/pangea/kubernetes/architecture.rb +383 -0
  17. data/lib/pangea/kubernetes/backend_registry.rb +117 -0
  18. data/lib/pangea/kubernetes/backends/aws_eks.rb +203 -0
  19. data/lib/pangea/kubernetes/backends/aws_nixos.rb +1347 -0
  20. data/lib/pangea/kubernetes/backends/azure_aks.rb +145 -0
  21. data/lib/pangea/kubernetes/backends/azure_nixos.rb +275 -0
  22. data/lib/pangea/kubernetes/backends/base.rb +116 -0
  23. data/lib/pangea/kubernetes/backends/gcp_gke.rb +176 -0
  24. data/lib/pangea/kubernetes/backends/gcp_nixos.rb +240 -0
  25. data/lib/pangea/kubernetes/backends/hcloud_k3s.rb +181 -0
  26. data/lib/pangea/kubernetes/backends/nixos_base.rb +235 -0
  27. data/lib/pangea/kubernetes/bare_metal/cloud_init.rb +196 -0
  28. data/lib/pangea/kubernetes/bare_metal/cluster_reference.rb +72 -0
  29. data/lib/pangea/kubernetes/load_balancer.rb +157 -0
  30. data/lib/pangea/kubernetes/network_backend_registry.rb +54 -0
  31. data/lib/pangea/kubernetes/network_backends/base.rb +78 -0
  32. data/lib/pangea/kubernetes/network_backends/cilium.rb +105 -0
  33. data/lib/pangea/kubernetes/network_backends/vpc_cni.rb +36 -0
  34. data/lib/pangea/kubernetes/types/argocd_config.rb +55 -0
  35. data/lib/pangea/kubernetes/types/control_plane_config.rb +65 -0
  36. data/lib/pangea/kubernetes/types/etcd_config.rb +64 -0
  37. data/lib/pangea/kubernetes/types/firewall_config.rb +39 -0
  38. data/lib/pangea/kubernetes/types/k3s_config.rb +112 -0
  39. data/lib/pangea/kubernetes/types/kernel_config.rb +31 -0
  40. data/lib/pangea/kubernetes/types/kubernetes_config.rb +129 -0
  41. data/lib/pangea/kubernetes/types/persistent_state_config.rb +100 -0
  42. data/lib/pangea/kubernetes/types/pki_config.rb +48 -0
  43. data/lib/pangea/kubernetes/types/secrets_config.rb +41 -0
  44. data/lib/pangea/kubernetes/types/vpn_config.rb +188 -0
  45. data/lib/pangea/kubernetes/types/wait_for_dns_config.rb +35 -0
  46. data/lib/pangea/kubernetes/types.rb +521 -0
  47. data/lib/pangea-kubernetes/version.rb +5 -0
  48. data/lib/pangea-kubernetes.rb +43 -0
  49. data/pangea-kubernetes.gemspec +33 -0
  50. metadata +192 -0
@@ -0,0 +1,521 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2025 The Pangea Authors
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'dry-struct'
18
+ require 'pangea/resources/types'
19
+ require 'pangea/kubernetes/types/firewall_config'
20
+ require 'pangea/kubernetes/types/kernel_config'
21
+ require 'pangea/kubernetes/types/wait_for_dns_config'
22
+ require 'pangea/kubernetes/types/etcd_config'
23
+ require 'pangea/kubernetes/types/pki_config'
24
+ require 'pangea/kubernetes/types/control_plane_config'
25
+ require 'pangea/kubernetes/types/secrets_config'
26
+ require 'pangea/kubernetes/types/k3s_config'
27
+ require 'pangea/kubernetes/types/kubernetes_config'
28
+ require 'pangea/kubernetes/types/argocd_config'
29
+ require 'pangea/kubernetes/types/vpn_config'
30
+ require 'pangea/kubernetes/types/persistent_state_config'
31
+
32
+ module Pangea
33
+ module Kubernetes
34
+ module Types
35
+ T = Pangea::Resources::Types
36
+
37
+ # Managed backends delegate to cloud-native K8s services (EKS, GKE, AKS)
38
+ MANAGED_BACKENDS = %i[aws gcp azure].freeze
39
+
40
+ # NixOS backends provision NixOS VMs with k3s/k8s via blackmatter-kubernetes
41
+ NIXOS_BACKENDS = %i[aws_nixos gcp_nixos azure_nixos hcloud].freeze
42
+
43
+ SUPPORTED_BACKENDS = (MANAGED_BACKENDS + NIXOS_BACKENDS).freeze
44
+
45
+ SUPPORTED_K8S_VERSIONS = %w[
46
+ 1.27 1.28 1.29 1.30 1.31 1.32 1.33 1.34
47
+ ].freeze
48
+
49
+ # Distributions supported by blackmatter-kubernetes
50
+ SUPPORTED_DISTRIBUTIONS = %i[k3s kubernetes].freeze
51
+
52
+ # Profiles from blackmatter-kubernetes lib/profiles.nix
53
+ SUPPORTED_PROFILES = %w[
54
+ cloud-server
55
+ flannel-minimal flannel-standard flannel-production
56
+ calico-standard calico-hardened
57
+ cilium-standard cilium-mesh
58
+ istio-mesh
59
+ ].freeze
60
+
61
+ # Node pool configuration — cloud-agnostic
62
+ class NodePoolConfig < Pangea::Resources::BaseAttributes
63
+ transform_keys(&:to_sym)
64
+
65
+ attribute :name, T::Coercible::Symbol
66
+ attribute :instance_types, T::Array.of(T::String).constrained(min_size: 1)
67
+ attribute :min_size, T::Coercible::Integer.constrained(gteq: 0).default(1)
68
+ attribute :max_size, T::Coercible::Integer.constrained(gteq: 1).default(3)
69
+ attribute :desired_size, T::Coercible::Integer.optional.default(nil)
70
+ attribute :disk_size_gb, T::Coercible::Integer.constrained(gteq: 10).default(20)
71
+ attribute :labels, T::Hash.default({}.freeze)
72
+ attribute :taints, T::Array.of(T::Hash).default([].freeze)
73
+ attribute :max_pods, T::Coercible::Integer.optional.default(nil)
74
+ attribute :ssh_keys, T::Array.of(T::String).default([].freeze)
75
+
76
+ def self.new(attributes)
77
+ attrs = attributes.is_a?(::Hash) ? attributes : {}
78
+ if attrs[:max_size] && attrs[:min_size] && attrs[:max_size] < attrs[:min_size]
79
+ raise Dry::Struct::Error, "max_size (#{attrs[:max_size]}) must be >= min_size (#{attrs[:min_size]})"
80
+ end
81
+ super(attrs)
82
+ end
83
+
84
+ def effective_desired_size
85
+ desired_size || min_size
86
+ end
87
+
88
+ def to_h
89
+ hash = {
90
+ name: name,
91
+ instance_types: instance_types,
92
+ min_size: min_size,
93
+ max_size: max_size,
94
+ disk_size_gb: disk_size_gb
95
+ }
96
+ hash[:desired_size] = desired_size if desired_size
97
+ hash[:labels] = labels if labels.any?
98
+ hash[:taints] = taints if taints.any?
99
+ hash[:max_pods] = max_pods if max_pods
100
+ hash[:ssh_keys] = ssh_keys if ssh_keys.any?
101
+ hash
102
+ end
103
+ end
104
+
105
+ # Addon configuration
106
+ class AddonConfig < Pangea::Resources::BaseAttributes
107
+ transform_keys(&:to_sym)
108
+
109
+ attribute :name, T::Coercible::Symbol
110
+ attribute :enabled, T::Bool.default(true)
111
+ attribute :version, T::String.optional.default(nil)
112
+ attribute :config, T::Hash.default({}.freeze)
113
+
114
+ def to_h
115
+ hash = { name: name, enabled: enabled }
116
+ hash[:version] = version if version
117
+ hash[:config] = config if config.any?
118
+ hash
119
+ end
120
+ end
121
+
122
+ # Network configuration — cloud-agnostic
123
+ class NetworkConfig < Pangea::Resources::BaseAttributes
124
+ transform_keys(&:to_sym)
125
+
126
+ attribute :vpc_cidr, T::String.optional.default(nil)
127
+ attribute :pod_cidr, T::String.optional.default(nil)
128
+ attribute :service_cidr, T::String.optional.default(nil)
129
+ attribute :subnet_ids, T::Array.of(T::String).default([].freeze)
130
+ attribute :security_group_ids, T::Array.of(T::String).default([].freeze)
131
+ attribute :private_endpoint, T::Bool.default(true)
132
+ attribute :public_endpoint, T::Bool.default(false)
133
+
134
+ def to_h
135
+ hash = {
136
+ private_endpoint: private_endpoint,
137
+ public_endpoint: public_endpoint
138
+ }
139
+ hash[:vpc_cidr] = vpc_cidr if vpc_cidr
140
+ hash[:pod_cidr] = pod_cidr if pod_cidr
141
+ hash[:service_cidr] = service_cidr if service_cidr
142
+ hash[:subnet_ids] = subnet_ids if subnet_ids.any?
143
+ hash[:security_group_ids] = security_group_ids if security_group_ids.any?
144
+ hash
145
+ end
146
+ end
147
+
148
+ # FluxCD GitOps bootstrap configuration
149
+ class FluxCDConfig < Pangea::Resources::BaseAttributes
150
+ transform_keys(&:to_sym)
151
+
152
+ attribute :enabled, T::Bool.default(true)
153
+ attribute :source_url, T::String
154
+ attribute :source_auth, T::String.constrained(included_in: %w[ssh token]).default('ssh')
155
+ attribute :source_interval, T::String.default('1m0s')
156
+ attribute :reconcile_path, T::String.default('./')
157
+ attribute :reconcile_interval, T::String.default('2m0s')
158
+ attribute :sops_enabled, T::Bool.default(true)
159
+
160
+ # Git branch to track
161
+ attribute :source_branch, T::String.default('main')
162
+
163
+ # Enable pruning during reconciliation
164
+ attribute :reconcile_prune, T::Bool.default(true)
165
+
166
+ # SSH known hosts content for git source
167
+ attribute :known_hosts, T::String.optional.default(nil)
168
+
169
+ # Path to SSH key file (sops-nix decrypted path on NixOS)
170
+ attribute :source_ssh_key_file, T::String.optional.default(nil)
171
+
172
+ # Path to token file (sops-nix decrypted path on NixOS)
173
+ attribute :source_token_file, T::String.optional.default(nil)
174
+
175
+ # Username for token-based auth
176
+ attribute :source_token_username, T::String.default('git')
177
+
178
+ # Path to SOPS age key file (sops-nix decrypted path on NixOS)
179
+ attribute :sops_age_key_file, T::String.optional.default(nil)
180
+
181
+ def to_h
182
+ hash = {
183
+ enabled: enabled,
184
+ source_url: source_url,
185
+ source_auth: source_auth,
186
+ source_interval: source_interval,
187
+ reconcile_path: reconcile_path,
188
+ reconcile_interval: reconcile_interval,
189
+ sops_enabled: sops_enabled,
190
+ source_branch: source_branch,
191
+ reconcile_prune: reconcile_prune,
192
+ source_token_username: source_token_username
193
+ }
194
+ hash[:known_hosts] = known_hosts if known_hosts
195
+ hash[:source_ssh_key_file] = source_ssh_key_file if source_ssh_key_file
196
+ hash[:source_token_file] = source_token_file if source_token_file
197
+ hash[:sops_age_key_file] = sops_age_key_file if sops_age_key_file
198
+ hash
199
+ end
200
+ end
201
+
202
+ # NixOS-specific configuration for blackmatter-kubernetes modules
203
+ class NixOSConfig < Pangea::Resources::BaseAttributes
204
+ transform_keys(&:to_sym)
205
+
206
+ attribute :image_id, T::String.optional.default(nil)
207
+ attribute :flake_url, T::String.optional.default(nil)
208
+ attribute :extra_modules, T::Array.of(T::String).default([].freeze)
209
+ attribute :sops_age_key_secret, T::String.optional.default(nil)
210
+ attribute :flux_ssh_key_secret, T::String.optional.default(nil)
211
+
212
+ # K3s distribution options (when distribution == :k3s)
213
+ attribute :k3s, K3sConfig.optional.default(nil)
214
+
215
+ # Vanilla Kubernetes distribution options (when distribution == :kubernetes)
216
+ attribute :kubernetes, VanillaKubernetesConfig.optional.default(nil)
217
+
218
+ # Secrets configuration (sops-nix path references)
219
+ attribute :secrets, SecretsConfig.optional.default(nil)
220
+
221
+ def to_h
222
+ hash = {}
223
+ hash[:image_id] = image_id if image_id
224
+ hash[:flake_url] = flake_url if flake_url
225
+ hash[:extra_modules] = extra_modules if extra_modules.any?
226
+ hash[:sops_age_key_secret] = sops_age_key_secret if sops_age_key_secret
227
+ hash[:flux_ssh_key_secret] = flux_ssh_key_secret if flux_ssh_key_secret
228
+ hash[:k3s] = k3s.to_h if k3s
229
+ hash[:kubernetes] = kubernetes.to_h if kubernetes
230
+ hash[:secrets] = secrets.to_h if secrets
231
+ hash
232
+ end
233
+ end
234
+
235
+ # Cluster-level configuration — cloud-agnostic
236
+ class ClusterConfig < Pangea::Resources::BaseAttributes
237
+ transform_keys(&:to_sym)
238
+
239
+ attribute :backend, T::Coercible::Symbol.constrained(included_in: SUPPORTED_BACKENDS)
240
+ attribute :kubernetes_version, T::String.constrained(included_in: SUPPORTED_K8S_VERSIONS).default('1.29')
241
+ attribute :region, T::String
242
+ attribute :node_pools, T::Array.of(NodePoolConfig).constrained(min_size: 1)
243
+ attribute :network, NetworkConfig.optional.default(nil)
244
+
245
+ # Pre-built network result — when set, Phase 1 (create_network) is skipped.
246
+ # The template must provide a Pangea::Contracts::NetworkResult (or subclass).
247
+ # This enables external network architectures (e.g., SecureVpc from
248
+ # pangea-architectures) to be composed with kubernetes_cluster().
249
+ attribute :external_network, T::Any.optional.default(nil)
250
+
251
+ attribute :addons, T::Array.of(T::Coercible::Symbol).default([].freeze)
252
+ attribute :tags, T::Hash.default({}.freeze)
253
+ attribute :encryption_at_rest, T::Bool.default(true)
254
+ attribute :logging, T::Array.of(T::String).default([].freeze)
255
+
256
+ # Distribution: k3s or vanilla kubernetes (NixOS backends only)
257
+ attribute :distribution, T::Coercible::Symbol.constrained(
258
+ included_in: SUPPORTED_DISTRIBUTIONS
259
+ ).default(:k3s)
260
+
261
+ # Blackmatter-kubernetes profile (NixOS backends only)
262
+ attribute :profile, T::String.constrained(
263
+ included_in: SUPPORTED_PROFILES
264
+ ).default('cloud-server')
265
+
266
+ # Distribution version track (e.g., '1.34', '1.35')
267
+ attribute :distribution_track, T::String.optional.default(nil)
268
+
269
+ # GitOps operator selection (:fluxcd or :argocd, default: :fluxcd)
270
+ attribute :gitops_operator, T::Coercible::Symbol.constrained(
271
+ included_in: %i[fluxcd argocd none]
272
+ ).default(:fluxcd)
273
+
274
+ # FluxCD GitOps bootstrap (NixOS backends only)
275
+ attribute :fluxcd, FluxCDConfig.optional.default(nil)
276
+
277
+ # ArgoCD GitOps bootstrap (NixOS backends only)
278
+ attribute :argocd, ArgocdConfig.optional.default(nil)
279
+
280
+ # Enable Karpenter IRSA IAM role at Terraform time (AWS only).
281
+ # Karpenter itself is deployed post-cluster via GitOps.
282
+ attribute :karpenter_enabled, T::Bool.default(false)
283
+
284
+ # Enable etcd backup S3 bucket creation (AWS only).
285
+ # Default off for cost savings. Production profiles should enable.
286
+ attribute :etcd_backup_enabled, T::Bool.default(false)
287
+
288
+ # Enable S3 versioning on the etcd backup bucket.
289
+ # Default off for cost savings. Production should keep this on.
290
+ attribute :etcd_backup_versioning, T::Bool.default(false)
291
+
292
+ # ── Load Balancing ─────────────────────────────────────────
293
+ # ALB for HTTP/HTTPS ingress traffic (public → web tier nodes)
294
+ attribute :ingress_alb_enabled, T::Bool.default(false)
295
+ attribute :ingress_alb_certificate_arn, T::String.optional.default(nil)
296
+ attribute :ingress_alb_idle_timeout, (T::Coercible::Integer | T::Coercible::Float).default(60)
297
+ attribute :ingress_alb_http_redirect, T::Bool.default(true)
298
+
299
+ # VPN NLB for WireGuard operator access (public, UDP)
300
+ attribute :vpn_nlb_enabled, T::Bool.default(false)
301
+ attribute :vpn_nlb_port, (T::Coercible::Integer | T::Coercible::Float).default(51822)
302
+
303
+ # Elastic IP allocation IDs for VPN NLB subnet_mapping.
304
+ # When provided, the VPN NLB uses subnet_mapping with these EIPs
305
+ # instead of plain subnets, giving the NLB a permanent public IP
306
+ # that survives destroy/recreate cycles (when EIPs are managed
307
+ # in a separate Terraform state like the packer workspace).
308
+ attribute :vpn_eip_allocation_ids, T::Array.of(T::String).default([].freeze)
309
+
310
+ # Internal K8s API NLB is always created (required for worker join)
311
+
312
+ # ── Security Hardening ────────────────────────────────────────
313
+
314
+ # Restrict node SG HTTP/HTTPS to ALB SG source (not 0.0.0.0/0).
315
+ # Only effective when ingress_alb_enabled is also true.
316
+ # Default on — when ALB exists, nodes should only accept traffic from it.
317
+ attribute :sg_restrict_http_to_alb, T::Bool.default(true)
318
+
319
+ # Source CIDR for WireGuard VPN NLB ingress (internet-facing).
320
+ # nil = 0.0.0.0/0 (current default). Set to operator IP range for hardening.
321
+ attribute :vpn_source_cidr, T::String.optional.default(nil)
322
+
323
+ # Enable VPC flow logs for network traffic auditing.
324
+ attribute :flow_logs_enabled, T::Bool.default(false)
325
+ attribute :flow_logs_traffic_type, T::String.constrained(
326
+ included_in: %w[ALL ACCEPT REJECT]
327
+ ).default('ALL')
328
+ attribute :flow_logs_retention_days, (T::Coercible::Integer | T::Coercible::Float).default(30)
329
+
330
+ # Enable KMS encryption for CloudWatch log groups.
331
+ # When true + kms_key_arn nil → creates a new KMS key with rotation.
332
+ attribute :kms_logs_enabled, T::Bool.default(false)
333
+ attribute :kms_key_arn, T::String.optional.default(nil)
334
+
335
+ # Create one NAT gateway per AZ (HA). false = single NAT in public-a.
336
+ attribute :nat_per_az, T::Bool.default(false)
337
+
338
+ # SSM-only access: no SSH key pair, no port 22 SG rule.
339
+ attribute :ssm_only, T::Bool.default(false)
340
+
341
+ # Separate S3 bucket for SSM session logs (nil = reuse etcd backup bucket).
342
+ attribute :ssm_logs_bucket, T::String.optional.default(nil)
343
+
344
+ # VPN target group health check (default: match vpn_nlb_port, not SSH 22).
345
+ attribute :vpn_health_check_port, (T::Coercible::Integer | T::Coercible::Float).optional.default(nil)
346
+
347
+ # Source CIDR for all internet-facing ingress (ALB, node HTTP/HTTPS).
348
+ # nil = 0.0.0.0/0 (open). Set to operator IP/32 to lock down the entire perimeter.
349
+ attribute :ingress_source_cidr, T::String.optional.default(nil)
350
+
351
+ # ACM certificate domain for ALB HTTPS (creates cert when set + no certificate_arn).
352
+ attribute :ingress_alb_domain, T::String.optional.default(nil)
353
+ attribute :ingress_alb_zone_id, T::String.optional.default(nil)
354
+
355
+ # Bootstrap secrets delivered via cloud-init for first-boot trust chain.
356
+ # Written to disk before sops-nix activates. Never included in resource tags.
357
+ # Keys: sops_age_key (cluster age private key), flux_github_token (GitHub PAT)
358
+ attribute :bootstrap_secrets, T::Hash.default({}.freeze)
359
+
360
+ # NixOS configuration (NixOS backends only)
361
+ attribute :nixos, NixOSConfig.optional.default(nil)
362
+
363
+ # VPN configuration (WireGuard links for operator access)
364
+ attribute :vpn, VpnConfig.optional.default(nil)
365
+
366
+ # Persistent state volume — opt-in EBS volume decoupled from the
367
+ # instance lifecycle. Survives ASG sleep/wake + instance churn.
368
+ # See PersistentStateConfig for the full slot surface.
369
+ attribute :persistent_state, PersistentStateConfig.optional.default(nil)
370
+
371
+ # ── Infrastructure parameters (NOT tags — typed config fields) ──
372
+ # These were previously smuggled through the tags hash.
373
+ # Now they're proper typed attributes that don't pollute resource tags.
374
+
375
+ # AWS account ID for IAM policy scoping (12-digit string)
376
+ attribute :account_id, T::String.optional.default(nil)
377
+
378
+ # S3 bucket name for etcd backups (when etcd_backup_enabled)
379
+ attribute :etcd_backup_bucket, T::String.optional.default(nil)
380
+
381
+ # CIDR for SSH access restriction (e.g., '10.0.0.0/8')
382
+ attribute :ssh_cidr, T::String.default('10.0.0.0/8')
383
+
384
+ # CIDR for K8s API access restriction
385
+ attribute :api_cidr, T::String.default('10.0.0.0/8')
386
+
387
+ # VPN CIDR for WireGuard tunnel (e.g., '10.100.3.0/24')
388
+ attribute :vpn_cidr, T::String.optional.default(nil)
389
+
390
+ # AWS-specific (managed EKS or NixOS EC2)
391
+ attribute :role_arn, T::String.optional.default(nil)
392
+ attribute :ami_id, T::String.optional.default(nil)
393
+ attribute :ssm_ami_parameter, T::String.optional.default(nil)
394
+ attribute :key_pair, T::String.optional.default(nil)
395
+
396
+ # GCP-specific (managed GKE or NixOS GCE)
397
+ attribute :project, T::String.optional.default(nil)
398
+ attribute :gce_image, T::String.optional.default(nil)
399
+
400
+ # Azure-specific (managed AKS or NixOS VMs)
401
+ attribute :resource_group_name, T::String.optional.default(nil)
402
+ attribute :dns_prefix, T::String.optional.default(nil)
403
+ attribute :azure_image_id, T::String.optional.default(nil)
404
+
405
+ def managed_kubernetes?
406
+ MANAGED_BACKENDS.include?(backend)
407
+ end
408
+
409
+ def nixos_backend?
410
+ NIXOS_BACKENDS.include?(backend)
411
+ end
412
+
413
+ def system_node_pool
414
+ node_pools.find { |np| np.name == :system } || node_pools.first
415
+ end
416
+
417
+ def worker_node_pools
418
+ node_pools.reject { |np| np.name == :system }
419
+ end
420
+
421
+ def self.new(attributes)
422
+ instance = super
423
+ instance.vpn&.validate! if instance.vpn
424
+ instance
425
+ end
426
+
427
+ def to_h
428
+ hash = {
429
+ backend: backend,
430
+ kubernetes_version: kubernetes_version,
431
+ region: region,
432
+ node_pools: node_pools.map(&:to_h)
433
+ }
434
+ hash[:network] = network.to_h if network
435
+ hash[:addons] = addons if addons.any?
436
+ hash[:tags] = tags if tags.any?
437
+ hash[:encryption_at_rest] = encryption_at_rest
438
+ hash[:logging] = logging if logging.any?
439
+ hash[:distribution] = distribution
440
+ hash[:profile] = profile
441
+ hash[:distribution_track] = distribution_track if distribution_track
442
+ hash[:fluxcd] = fluxcd.to_h if fluxcd
443
+ hash[:nixos] = nixos.to_h if nixos
444
+ hash[:vpn] = vpn.to_h if vpn && vpn.links.any?
445
+ hash[:persistent_state] = persistent_state.to_h if persistent_state
446
+ hash[:role_arn] = role_arn if role_arn
447
+ hash[:ami_id] = ami_id if ami_id
448
+ hash[:ssm_ami_parameter] = ssm_ami_parameter if ssm_ami_parameter
449
+ hash[:key_pair] = key_pair if key_pair
450
+ hash[:project] = project if project
451
+ hash[:gce_image] = gce_image if gce_image
452
+ hash[:resource_group_name] = resource_group_name if resource_group_name
453
+ hash[:dns_prefix] = dns_prefix if dns_prefix
454
+ hash[:azure_image_id] = azure_image_id if azure_image_id
455
+ hash
456
+ end
457
+ end
458
+
459
+ # Deployment context — metadata for the architecture reference
460
+ class DeploymentContext < Pangea::Resources::BaseAttributes
461
+ transform_keys(&:to_sym)
462
+
463
+ attribute :environment, T::Coercible::Symbol.constrained(included_in: %i[production staging development])
464
+ attribute :cluster_name, T::Coercible::Symbol
465
+ attribute :team, T::String.optional.default(nil)
466
+ attribute :cost_center, T::String.optional.default(nil)
467
+
468
+ def to_h
469
+ hash = { environment: environment, cluster_name: cluster_name }
470
+ hash[:team] = team if team
471
+ hash[:cost_center] = cost_center if cost_center
472
+ hash
473
+ end
474
+ end
475
+
476
+ # Load balancer configuration for elastic LB tier
477
+ class LoadBalancerConfig < Pangea::Resources::BaseAttributes
478
+ transform_keys(&:to_sym)
479
+
480
+ attribute :mode, T::String.constrained(included_in: %w[haproxy haproxy-bird]).default('haproxy')
481
+ attribute :instance_count, T::Coercible::Integer.constrained(gteq: 1).default(2)
482
+ attribute :instance_type, T::String
483
+ attribute :region, T::String
484
+ attribute :backends, T::Array.of(T::Hash).constrained(min_size: 1)
485
+ attribute :health_check_interval, T::String.default('5s')
486
+ attribute :max_connections, T::Coercible::Integer.default(50_000)
487
+ attribute :frontend_ports, T::Array.of(T::Coercible::Integer).default([80, 443].freeze)
488
+ attribute :tags, T::Hash.default({}.freeze)
489
+
490
+ # Bare metal BGP options
491
+ attribute :bgp_asn, T::Coercible::Integer.optional.default(nil)
492
+ attribute :bgp_neighbor, T::String.optional.default(nil)
493
+ attribute :vrrp_interface, T::String.optional.default(nil)
494
+ attribute :virtual_ips, T::Array.of(T::String).default([].freeze)
495
+
496
+ def bare_metal?
497
+ mode == 'haproxy-bird'
498
+ end
499
+
500
+ def to_h
501
+ hash = {
502
+ mode: mode,
503
+ instance_count: instance_count,
504
+ instance_type: instance_type,
505
+ region: region,
506
+ backends: backends,
507
+ health_check_interval: health_check_interval,
508
+ max_connections: max_connections,
509
+ frontend_ports: frontend_ports
510
+ }
511
+ hash[:tags] = tags if tags.any?
512
+ hash[:bgp_asn] = bgp_asn if bgp_asn
513
+ hash[:bgp_neighbor] = bgp_neighbor if bgp_neighbor
514
+ hash[:vrrp_interface] = vrrp_interface if vrrp_interface
515
+ hash[:virtual_ips] = virtual_ips if virtual_ips.any?
516
+ hash
517
+ end
518
+ end
519
+ end
520
+ end
521
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PangeaKubernetes
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2025 The Pangea Authors
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'pangea-core'
18
+
19
+ # Core types and framework
20
+ require 'pangea/kubernetes/types'
21
+ require 'pangea/kubernetes/backend_registry'
22
+ require 'pangea/kubernetes/backends/base'
23
+ require 'pangea/kubernetes/backends/nixos_base'
24
+
25
+ # Architecture (user-facing API)
26
+ require 'pangea/kubernetes/architecture'
27
+
28
+ # Bare metal support
29
+ require 'pangea/kubernetes/bare_metal/cloud_init'
30
+ require 'pangea/kubernetes/bare_metal/cluster_reference'
31
+
32
+ # Elastic load balancer
33
+ require 'pangea/kubernetes/load_balancer'
34
+
35
+ # Network backends (CNI/mesh selection)
36
+ require 'pangea/kubernetes/network_backend_registry'
37
+ require 'pangea/kubernetes/network_backends/base'
38
+
39
+ # Compute backends are lazy-loaded by BackendRegistry — not required here.
40
+ # Network backends are lazy-loaded by NetworkBackendRegistry.
41
+ # Users only need the provider gem for backends they actually use.
42
+
43
+ require 'pangea-kubernetes/version'
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path(%(lib), __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require_relative %(lib/pangea-kubernetes/version)
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = %(pangea-kubernetes)
9
+ spec.version = PangeaKubernetes::VERSION
10
+ spec.authors = [%(Luis Zayas)]
11
+ spec.email = [%(drzthslnt@gmail.com)]
12
+ spec.description = %(Cloud-agnostic Kubernetes abstractions for Pangea. Compiles kubernetes_cluster() and kubernetes_node_pool() to provider-specific Terraform JSON via backend modules (AWS EKS, GCP GKE, Azure AKS, Hetzner k3s-on-NixOS).)
13
+ spec.summary = %(Cloud-agnostic Kubernetes abstractions for Pangea)
14
+ spec.homepage = %(https://github.com/pleme-io/pangea-kubernetes)
15
+ spec.license = %(Apache-2.0)
16
+ spec.require_paths = [%(lib)]
17
+ spec.required_ruby_version = %(>=3.3.0)
18
+
19
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
20
+ f.match(%r{^(test|spec|features)/})
21
+ end
22
+
23
+ spec.add_dependency "pangea-core", "~> 0.2"
24
+ spec.add_dependency "terraform-synthesizer", "~> 0.0.28"
25
+ spec.add_dependency "dry-types", "~> 1.7"
26
+ spec.add_dependency "dry-struct", "~> 1.6"
27
+
28
+ spec.add_development_dependency "rspec", "~> 3.12"
29
+ spec.add_development_dependency "rake", "~> 13.0"
30
+ spec.add_development_dependency "simplecov", "~> 0.22"
31
+
32
+ spec.metadata['rubygems_mfa_required'] = 'true'
33
+ end