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.
- checksums.yaml +7 -0
- data/.github/workflows/auto-bump.yml +11 -0
- data/.github/workflows/ci.yml +7 -0
- data/.github/workflows/release.yml +22 -0
- data/.gitignore +6 -0
- data/.rspec +3 -0
- data/AGENTS.md +3 -0
- data/CLAUDE.md +370 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +128 -0
- data/README.md +42 -0
- data/Rakefile +8 -0
- data/flake.lock +2144 -0
- data/flake.nix +30 -0
- data/gemset.nix +312 -0
- data/lib/pangea/kubernetes/architecture.rb +383 -0
- data/lib/pangea/kubernetes/backend_registry.rb +117 -0
- data/lib/pangea/kubernetes/backends/aws_eks.rb +203 -0
- data/lib/pangea/kubernetes/backends/aws_nixos.rb +1347 -0
- data/lib/pangea/kubernetes/backends/azure_aks.rb +145 -0
- data/lib/pangea/kubernetes/backends/azure_nixos.rb +275 -0
- data/lib/pangea/kubernetes/backends/base.rb +116 -0
- data/lib/pangea/kubernetes/backends/gcp_gke.rb +176 -0
- data/lib/pangea/kubernetes/backends/gcp_nixos.rb +240 -0
- data/lib/pangea/kubernetes/backends/hcloud_k3s.rb +181 -0
- data/lib/pangea/kubernetes/backends/nixos_base.rb +235 -0
- data/lib/pangea/kubernetes/bare_metal/cloud_init.rb +196 -0
- data/lib/pangea/kubernetes/bare_metal/cluster_reference.rb +72 -0
- data/lib/pangea/kubernetes/load_balancer.rb +157 -0
- data/lib/pangea/kubernetes/network_backend_registry.rb +54 -0
- data/lib/pangea/kubernetes/network_backends/base.rb +78 -0
- data/lib/pangea/kubernetes/network_backends/cilium.rb +105 -0
- data/lib/pangea/kubernetes/network_backends/vpc_cni.rb +36 -0
- data/lib/pangea/kubernetes/types/argocd_config.rb +55 -0
- data/lib/pangea/kubernetes/types/control_plane_config.rb +65 -0
- data/lib/pangea/kubernetes/types/etcd_config.rb +64 -0
- data/lib/pangea/kubernetes/types/firewall_config.rb +39 -0
- data/lib/pangea/kubernetes/types/k3s_config.rb +112 -0
- data/lib/pangea/kubernetes/types/kernel_config.rb +31 -0
- data/lib/pangea/kubernetes/types/kubernetes_config.rb +129 -0
- data/lib/pangea/kubernetes/types/persistent_state_config.rb +100 -0
- data/lib/pangea/kubernetes/types/pki_config.rb +48 -0
- data/lib/pangea/kubernetes/types/secrets_config.rb +41 -0
- data/lib/pangea/kubernetes/types/vpn_config.rb +188 -0
- data/lib/pangea/kubernetes/types/wait_for_dns_config.rb +35 -0
- data/lib/pangea/kubernetes/types.rb +521 -0
- data/lib/pangea-kubernetes/version.rb +5 -0
- data/lib/pangea-kubernetes.rb +43 -0
- data/pangea-kubernetes.gemspec +33 -0
- 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,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
|