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,383 @@
|
|
|
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/contracts'
|
|
18
|
+
require 'pangea/kubernetes/types'
|
|
19
|
+
require 'pangea/kubernetes/backend_registry'
|
|
20
|
+
require 'pangea/kubernetes/backends/base'
|
|
21
|
+
|
|
22
|
+
module Pangea
|
|
23
|
+
module Kubernetes
|
|
24
|
+
# Cloud-agnostic Kubernetes architecture module.
|
|
25
|
+
# Provides kubernetes_cluster() and kubernetes_node_pool() functions
|
|
26
|
+
# that delegate to provider-specific backends.
|
|
27
|
+
#
|
|
28
|
+
# This module is designed to be included in a synthesizer context
|
|
29
|
+
# (AbstractSynthesizer or TerraformSynthesizer).
|
|
30
|
+
#
|
|
31
|
+
# @example
|
|
32
|
+
# class MyInfra < TerraformSynthesizer
|
|
33
|
+
# include Pangea::Kubernetes::Architecture
|
|
34
|
+
#
|
|
35
|
+
# def build
|
|
36
|
+
# kubernetes_cluster(:production, {
|
|
37
|
+
# backend: :aws,
|
|
38
|
+
# kubernetes_version: '1.29',
|
|
39
|
+
# region: 'us-east-1',
|
|
40
|
+
# node_pools: [
|
|
41
|
+
# { name: :system, instance_types: ['t3.large'], min_size: 2, max_size: 5 }
|
|
42
|
+
# ]
|
|
43
|
+
# })
|
|
44
|
+
# end
|
|
45
|
+
# end
|
|
46
|
+
module Architecture
|
|
47
|
+
# Create a complete Kubernetes cluster with all supporting infrastructure.
|
|
48
|
+
#
|
|
49
|
+
# Phase pipeline: Network -> IAM -> Cluster -> Node Pools -> Addons
|
|
50
|
+
#
|
|
51
|
+
# @param name [Symbol] Architecture name
|
|
52
|
+
# @param attributes [Hash] Cluster configuration (see Types::ClusterConfig)
|
|
53
|
+
# @return [ArchitectureResult] with cluster, node_pools, network, iam references
|
|
54
|
+
def kubernetes_cluster(name, attributes = {})
|
|
55
|
+
# VPN config comes in two forms:
|
|
56
|
+
# 1. Typed (VpnConfig with :links) — validated by dry-struct, used by NixOS backends
|
|
57
|
+
# 2. Cloud-init passthrough (flat hash with :interface, :port, etc.) — passed
|
|
58
|
+
# as-is to cloud-init for AWS/cloud backends where the node configures WireGuard
|
|
59
|
+
# Both are legitimate. Only validate if the hash has :links (typed form).
|
|
60
|
+
|
|
61
|
+
config = Types::ClusterConfig.new(attributes)
|
|
62
|
+
backend_module = BackendRegistry.resolve(config.backend)
|
|
63
|
+
backend_module.load_provider!
|
|
64
|
+
|
|
65
|
+
base_tags = {
|
|
66
|
+
KubernetesCluster: name.to_s,
|
|
67
|
+
Backend: config.backend.to_s,
|
|
68
|
+
ManagedBy: 'Pangea'
|
|
69
|
+
}.merge(config.tags)
|
|
70
|
+
|
|
71
|
+
result = ArchitectureResult.new(name, config)
|
|
72
|
+
|
|
73
|
+
# Phase 1: Network
|
|
74
|
+
# Pre-built network takes priority (e.g., SecureVpc from pangea-architectures).
|
|
75
|
+
# When external_network is set, skip backend's create_network entirely.
|
|
76
|
+
if config.external_network
|
|
77
|
+
result.network = config.external_network
|
|
78
|
+
elsif config.network
|
|
79
|
+
result.network = backend_module.create_network(self, name, config, base_tags)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Phase 2: IAM
|
|
83
|
+
result.iam = backend_module.create_iam(self, name, config, base_tags)
|
|
84
|
+
|
|
85
|
+
# Phase 3: Cluster
|
|
86
|
+
result.cluster = backend_module.create_cluster(self, name, config, result, base_tags)
|
|
87
|
+
|
|
88
|
+
# Phase 4: Node Pools
|
|
89
|
+
config.node_pools.each do |pool_config|
|
|
90
|
+
pool_ref = backend_module.create_node_pool(self, name, result.cluster, pool_config, base_tags)
|
|
91
|
+
result.add_node_pool(pool_config.name, pool_ref)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
result
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Create a standalone node pool for an existing cluster.
|
|
98
|
+
#
|
|
99
|
+
# @param cluster_name [Symbol] Parent cluster name
|
|
100
|
+
# @param pool_name [Symbol] Node pool name
|
|
101
|
+
# @param attributes [Hash] Node pool configuration
|
|
102
|
+
# @param cluster_ref [ResourceReference] Reference to existing cluster
|
|
103
|
+
# @param backend [Symbol] Backend name (:aws, :gcp, :azure, :hcloud)
|
|
104
|
+
# @return [ResourceReference] Node pool reference
|
|
105
|
+
def kubernetes_node_pool(cluster_name, pool_name, attributes = {}, cluster_ref:, backend:, tags: {})
|
|
106
|
+
pool_config = Types::NodePoolConfig.new(attributes.merge(name: pool_name))
|
|
107
|
+
backend_module = BackendRegistry.resolve(backend)
|
|
108
|
+
backend_module.load_provider!
|
|
109
|
+
|
|
110
|
+
base_tags = {
|
|
111
|
+
KubernetesCluster: cluster_name.to_s,
|
|
112
|
+
Backend: backend.to_s,
|
|
113
|
+
ManagedBy: 'Pangea'
|
|
114
|
+
}.merge(tags)
|
|
115
|
+
|
|
116
|
+
backend_module.create_node_pool(self, cluster_name, cluster_ref, pool_config, base_tags)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# AWS-specific NetworkResult — extends the base contract with AWS fields.
|
|
120
|
+
# is_a?(Pangea::Contracts::NetworkResult) returns true.
|
|
121
|
+
class NetworkResult < Pangea::Contracts::NetworkResult
|
|
122
|
+
attr_accessor :igw, :route_table, :etcd_bucket,
|
|
123
|
+
:flow_log, :flow_log_role,
|
|
124
|
+
:ssm_logs_bucket,
|
|
125
|
+
:kms_key,
|
|
126
|
+
:persistent_state_volume
|
|
127
|
+
|
|
128
|
+
def initialize
|
|
129
|
+
super
|
|
130
|
+
@igw = nil
|
|
131
|
+
@route_table = nil
|
|
132
|
+
@etcd_bucket = nil
|
|
133
|
+
@flow_log = nil
|
|
134
|
+
@flow_log_role = nil
|
|
135
|
+
@ssm_logs_bucket = nil
|
|
136
|
+
@kms_key = nil
|
|
137
|
+
@persistent_state_volume = nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def [](key)
|
|
141
|
+
case key.to_sym
|
|
142
|
+
when :igw then igw
|
|
143
|
+
when :route_table then route_table
|
|
144
|
+
when :etcd_bucket then etcd_bucket
|
|
145
|
+
when :flow_log then flow_log
|
|
146
|
+
when :flow_log_role then flow_log_role
|
|
147
|
+
when :ssm_logs_bucket then ssm_logs_bucket
|
|
148
|
+
when :kms_key then kms_key
|
|
149
|
+
when :persistent_state_volume then persistent_state_volume
|
|
150
|
+
else super
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def to_h
|
|
155
|
+
hash = super
|
|
156
|
+
hash[:igw] = igw if igw
|
|
157
|
+
hash[:route_table] = route_table if route_table
|
|
158
|
+
hash[:etcd_bucket] = etcd_bucket if etcd_bucket
|
|
159
|
+
hash[:flow_log] = flow_log if flow_log
|
|
160
|
+
hash[:flow_log_role] = flow_log_role if flow_log_role
|
|
161
|
+
hash[:ssm_logs_bucket] = ssm_logs_bucket if ssm_logs_bucket
|
|
162
|
+
hash[:kms_key] = kms_key if kms_key
|
|
163
|
+
hash[:persistent_state_volume] = persistent_state_volume if persistent_state_volume
|
|
164
|
+
hash
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# GCP-specific NetworkResult with firewall rules
|
|
169
|
+
class GcpNetworkResult < Pangea::Contracts::NetworkResult
|
|
170
|
+
attr_accessor :firewall_internal, :firewall_external
|
|
171
|
+
|
|
172
|
+
def [](key)
|
|
173
|
+
case key.to_sym
|
|
174
|
+
when :firewall_internal then firewall_internal
|
|
175
|
+
when :firewall_external then firewall_external
|
|
176
|
+
else super
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def to_h
|
|
181
|
+
hash = super
|
|
182
|
+
hash[:firewall_internal] = firewall_internal if firewall_internal
|
|
183
|
+
hash[:firewall_external] = firewall_external if firewall_external
|
|
184
|
+
hash
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Azure-specific NetworkResult with resource group, vnet, and NSG
|
|
189
|
+
class AzureNetworkResult < Pangea::Contracts::NetworkResult
|
|
190
|
+
attr_accessor :resource_group, :vnet, :nsg
|
|
191
|
+
|
|
192
|
+
def [](key)
|
|
193
|
+
case key.to_sym
|
|
194
|
+
when :resource_group then resource_group
|
|
195
|
+
when :vnet then vnet
|
|
196
|
+
when :nsg then nsg
|
|
197
|
+
else super
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def to_h
|
|
202
|
+
hash = super
|
|
203
|
+
hash[:resource_group] = resource_group if resource_group
|
|
204
|
+
hash[:vnet] = vnet if vnet
|
|
205
|
+
hash[:nsg] = nsg if nsg
|
|
206
|
+
hash
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Hetzner Cloud-specific NetworkResult with network (not VPC)
|
|
211
|
+
class HcloudNetworkResult < Pangea::Contracts::NetworkResult
|
|
212
|
+
attr_accessor :network
|
|
213
|
+
|
|
214
|
+
def [](key)
|
|
215
|
+
case key.to_sym
|
|
216
|
+
when :network then network
|
|
217
|
+
else super
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def to_h
|
|
222
|
+
hash = super
|
|
223
|
+
hash[:network] = network if network
|
|
224
|
+
hash
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# AWS-specific IamResult — extends the base contract with AWS fields.
|
|
229
|
+
# is_a?(Pangea::Contracts::IamResult) returns true.
|
|
230
|
+
class IamResult < Pangea::Contracts::IamResult
|
|
231
|
+
attr_accessor :log_group,
|
|
232
|
+
:ecr_policy, :etcd_policy, :logs_policy,
|
|
233
|
+
:ec2_policy, :ssm_policy,
|
|
234
|
+
:karpenter_role, :karpenter_profile,
|
|
235
|
+
:persistent_state_policy
|
|
236
|
+
|
|
237
|
+
def initialize
|
|
238
|
+
super
|
|
239
|
+
@log_group = nil
|
|
240
|
+
@ecr_policy = nil
|
|
241
|
+
@etcd_policy = nil
|
|
242
|
+
@logs_policy = nil
|
|
243
|
+
@ec2_policy = nil
|
|
244
|
+
@ssm_policy = nil
|
|
245
|
+
@karpenter_role = nil
|
|
246
|
+
@karpenter_profile = nil
|
|
247
|
+
@persistent_state_policy = nil
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Hash-style access for backward compatibility
|
|
251
|
+
def [](key)
|
|
252
|
+
case key.to_sym
|
|
253
|
+
when :log_group then log_group
|
|
254
|
+
when :ecr_policy then ecr_policy
|
|
255
|
+
when :etcd_policy then etcd_policy
|
|
256
|
+
when :logs_policy then logs_policy
|
|
257
|
+
when :ec2_policy then ec2_policy
|
|
258
|
+
when :ssm_policy then ssm_policy
|
|
259
|
+
when :karpenter_role then karpenter_role
|
|
260
|
+
when :karpenter_profile then karpenter_profile
|
|
261
|
+
when :persistent_state_policy then persistent_state_policy
|
|
262
|
+
else super
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def to_h
|
|
267
|
+
hash = super
|
|
268
|
+
hash[:log_group] = log_group if log_group
|
|
269
|
+
hash[:ecr_policy] = ecr_policy if ecr_policy
|
|
270
|
+
hash[:etcd_policy] = etcd_policy if etcd_policy
|
|
271
|
+
hash[:logs_policy] = logs_policy if logs_policy
|
|
272
|
+
hash[:ec2_policy] = ec2_policy if ec2_policy
|
|
273
|
+
hash[:ssm_policy] = ssm_policy if ssm_policy
|
|
274
|
+
hash[:karpenter_role] = karpenter_role if karpenter_role
|
|
275
|
+
hash[:karpenter_profile] = karpenter_profile if karpenter_profile
|
|
276
|
+
hash[:persistent_state_policy] = persistent_state_policy if persistent_state_policy
|
|
277
|
+
hash
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# AWS EKS-specific IamResult with cluster_role, cluster_policy_attachment, and node_role
|
|
282
|
+
class AwsEksIamResult < IamResult
|
|
283
|
+
attr_accessor :cluster_role, :cluster_policy_attachment, :node_role
|
|
284
|
+
|
|
285
|
+
def [](key)
|
|
286
|
+
case key.to_sym
|
|
287
|
+
when :cluster_role then cluster_role
|
|
288
|
+
when :cluster_policy_attachment then cluster_policy_attachment
|
|
289
|
+
when :node_role then node_role
|
|
290
|
+
else super
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def to_h
|
|
295
|
+
hash = super
|
|
296
|
+
hash[:cluster_role] = cluster_role if cluster_role
|
|
297
|
+
hash[:cluster_policy_attachment] = cluster_policy_attachment if cluster_policy_attachment
|
|
298
|
+
hash[:node_role] = node_role if node_role
|
|
299
|
+
hash
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# GCP-specific IamResult with service account for nodes
|
|
304
|
+
class GcpIamResult < IamResult
|
|
305
|
+
attr_accessor :node_sa
|
|
306
|
+
|
|
307
|
+
def [](key)
|
|
308
|
+
case key.to_sym
|
|
309
|
+
when :node_sa then node_sa
|
|
310
|
+
else super
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def to_h
|
|
315
|
+
hash = super
|
|
316
|
+
hash[:node_sa] = node_sa if node_sa
|
|
317
|
+
hash
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# AWS-specific ClusterResult — extends the base contract with AWS fields.
|
|
322
|
+
# is_a?(Pangea::Contracts::ClusterResult) returns true.
|
|
323
|
+
class ClusterResult < Pangea::Contracts::ClusterResult
|
|
324
|
+
def asg_tg
|
|
325
|
+
control_plane_ref.asg_tg
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def subnet_ids
|
|
329
|
+
control_plane_ref.subnet_ids
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def instance_profile_name
|
|
333
|
+
control_plane_ref.instance_profile_name
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def ami_id
|
|
337
|
+
control_plane_ref.ami_id
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def key_name
|
|
341
|
+
control_plane_ref.key_name
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def ipv4_address
|
|
345
|
+
control_plane_ref.ipv4_address
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Minimal accessor so templates can write result.cluster.security_group.id
|
|
350
|
+
# Inherits from the base contract for is_a? compatibility.
|
|
351
|
+
SecurityGroupAccessor = Pangea::Contracts::SecurityGroupAccessor
|
|
352
|
+
|
|
353
|
+
# Result object from kubernetes_cluster() — holds all created references.
|
|
354
|
+
# Inherits from base contract; provider-specific to_h calls config methods
|
|
355
|
+
# that the typed ClusterConfig provides.
|
|
356
|
+
class ArchitectureResult < Pangea::Contracts::ArchitectureResult
|
|
357
|
+
# Override cluster= to wrap in the local ClusterResult subclass
|
|
358
|
+
# (not the base Pangea::Contracts::ClusterResult)
|
|
359
|
+
def cluster=(value)
|
|
360
|
+
@cluster = if value.is_a?(Pangea::Contracts::ClusterResult)
|
|
361
|
+
value
|
|
362
|
+
elsif value
|
|
363
|
+
ClusterResult.new(value)
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def to_h
|
|
368
|
+
{
|
|
369
|
+
name: name,
|
|
370
|
+
backend: config.backend,
|
|
371
|
+
kubernetes_version: config.kubernetes_version,
|
|
372
|
+
region: config.region,
|
|
373
|
+
managed_kubernetes: config.managed_kubernetes?,
|
|
374
|
+
cluster: cluster&.to_h,
|
|
375
|
+
network: network_to_h,
|
|
376
|
+
iam: iam_to_h,
|
|
377
|
+
node_pools: node_pools.transform_values { |np| np.respond_to?(:to_h) ? np.to_h : np }
|
|
378
|
+
}
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
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
|
+
module Pangea
|
|
18
|
+
module Kubernetes
|
|
19
|
+
# Lazy-loading backend registry. Provider gems are loaded on first use,
|
|
20
|
+
# so users only need the gems for backends they actually reference.
|
|
21
|
+
module BackendRegistry
|
|
22
|
+
@backends = {}
|
|
23
|
+
|
|
24
|
+
# Backend matrix: cloud x tech
|
|
25
|
+
#
|
|
26
|
+
# Each cloud provider can run multiple Kubernetes technologies:
|
|
27
|
+
# - Managed K8s (EKS, GKE, AKS) — cloud-native control plane
|
|
28
|
+
# - NixOS K3s — lightweight K3s on NixOS VMs via blackmatter-kubernetes
|
|
29
|
+
# - NixOS K8s — vanilla Kubernetes on NixOS VMs (future)
|
|
30
|
+
#
|
|
31
|
+
# Naming: {cloud}_{tech} (e.g., aws_eks, aws_nixos_k3s, gcp_gke)
|
|
32
|
+
# Legacy aliases: :aws → :aws_eks, :aws_nixos → :aws_nixos_k3s
|
|
33
|
+
BACKEND_MAP = {
|
|
34
|
+
# ── AWS ───────────────────────────────────────────────────────
|
|
35
|
+
aws_eks: { module_path: 'pangea/kubernetes/backends/aws_eks', class_name: 'AwsEks' },
|
|
36
|
+
aws_nixos_k3s: { module_path: 'pangea/kubernetes/backends/aws_nixos', class_name: 'AwsNixos' },
|
|
37
|
+
# aws_nixos_k8s: { module_path: 'pangea/kubernetes/backends/aws_nixos_k8s', class_name: 'AwsNixosK8s' },
|
|
38
|
+
|
|
39
|
+
# ── GCP ───────────────────────────────────────────────────────
|
|
40
|
+
gcp_gke: { module_path: 'pangea/kubernetes/backends/gcp_gke', class_name: 'GcpGke' },
|
|
41
|
+
gcp_nixos_k3s: { module_path: 'pangea/kubernetes/backends/gcp_nixos', class_name: 'GcpNixos' },
|
|
42
|
+
|
|
43
|
+
# ── Azure ─────────────────────────────────────────────────────
|
|
44
|
+
azure_aks: { module_path: 'pangea/kubernetes/backends/azure_aks', class_name: 'AzureAks' },
|
|
45
|
+
azure_nixos_k3s: { module_path: 'pangea/kubernetes/backends/azure_nixos', class_name: 'AzureNixos' },
|
|
46
|
+
|
|
47
|
+
# ── Hetzner ──────────────────────────────────────────────────
|
|
48
|
+
hcloud_k3s: { module_path: 'pangea/kubernetes/backends/hcloud_k3s', class_name: 'HcloudK3s' },
|
|
49
|
+
}.freeze
|
|
50
|
+
|
|
51
|
+
# Legacy aliases — backward compat with existing templates
|
|
52
|
+
ALIASES = {
|
|
53
|
+
aws: :aws_eks,
|
|
54
|
+
aws_nixos: :aws_nixos_k3s,
|
|
55
|
+
gcp: :gcp_gke,
|
|
56
|
+
gcp_nixos: :gcp_nixos_k3s,
|
|
57
|
+
azure: :azure_aks,
|
|
58
|
+
azure_nixos: :azure_nixos_k3s,
|
|
59
|
+
hcloud: :hcloud_k3s,
|
|
60
|
+
}.freeze
|
|
61
|
+
|
|
62
|
+
class << self
|
|
63
|
+
# Register a backend module for a given backend name
|
|
64
|
+
def register(name, backend_module)
|
|
65
|
+
@backends[name.to_sym] = backend_module
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Resolve a backend by name, lazy-loading if needed.
|
|
69
|
+
# Supports both canonical names (aws_eks) and legacy aliases (aws).
|
|
70
|
+
def resolve(name)
|
|
71
|
+
name = name.to_sym
|
|
72
|
+
# Resolve alias to canonical name
|
|
73
|
+
name = ALIASES[name] if ALIASES.key?(name)
|
|
74
|
+
|
|
75
|
+
return @backends[name] if @backends.key?(name)
|
|
76
|
+
|
|
77
|
+
entry = BACKEND_MAP[name]
|
|
78
|
+
raise ArgumentError, "Unknown backend: #{name}. Available: #{available_backends.join(', ')}" unless entry
|
|
79
|
+
|
|
80
|
+
load_backend(name, entry)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# List all canonical backend names
|
|
84
|
+
def available_backends
|
|
85
|
+
BACKEND_MAP.keys
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# List all names including aliases
|
|
89
|
+
def all_names
|
|
90
|
+
(BACKEND_MAP.keys + ALIASES.keys).uniq
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Check if a backend's provider gem is available
|
|
94
|
+
def backend_available?(name)
|
|
95
|
+
resolve(name)
|
|
96
|
+
true
|
|
97
|
+
rescue LoadError
|
|
98
|
+
false
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Reset registry (for testing)
|
|
102
|
+
def reset!
|
|
103
|
+
@backends = {}
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def load_backend(name, entry)
|
|
109
|
+
require entry[:module_path]
|
|
110
|
+
backend_module = Pangea::Kubernetes::Backends.const_get(entry[:class_name])
|
|
111
|
+
@backends[name] = backend_module
|
|
112
|
+
backend_module
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,203 @@
|
|
|
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/kubernetes/backends/base'
|
|
18
|
+
|
|
19
|
+
module Pangea
|
|
20
|
+
module Kubernetes
|
|
21
|
+
module Backends
|
|
22
|
+
# AWS EKS backend — creates managed EKS clusters with VPC, IAM, and node groups.
|
|
23
|
+
module AwsEks
|
|
24
|
+
include Base
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
def backend_name = :aws
|
|
28
|
+
def managed_kubernetes? = true
|
|
29
|
+
def required_gem = 'pangea-aws'
|
|
30
|
+
|
|
31
|
+
def load_provider!
|
|
32
|
+
require required_gem
|
|
33
|
+
rescue LoadError => e
|
|
34
|
+
raise LoadError,
|
|
35
|
+
"Backend :aws requires gem 'pangea-aws'. " \
|
|
36
|
+
"Add it to your Gemfile: gem 'pangea-aws'\n" \
|
|
37
|
+
"Original error: #{e.message}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Create VPC + subnets for the EKS cluster
|
|
41
|
+
def create_network(ctx, name, config, tags)
|
|
42
|
+
network = Architecture::NetworkResult.new
|
|
43
|
+
|
|
44
|
+
vpc_cidr = config.network&.vpc_cidr || '10.0.0.0/16'
|
|
45
|
+
network.vpc = ctx.aws_vpc(
|
|
46
|
+
:"#{name}_vpc",
|
|
47
|
+
cidr_block: vpc_cidr,
|
|
48
|
+
enable_dns_hostnames: true,
|
|
49
|
+
enable_dns_support: true,
|
|
50
|
+
tags: tags.merge(Name: "#{name}-vpc")
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Create 2 subnets in different AZs (EKS requirement)
|
|
54
|
+
%w[a b].each_with_index do |az_suffix, idx|
|
|
55
|
+
subnet = ctx.aws_subnet(
|
|
56
|
+
:"#{name}_subnet_#{az_suffix}",
|
|
57
|
+
vpc_id: network.vpc.id,
|
|
58
|
+
cidr_block: "10.0.#{idx}.0/24",
|
|
59
|
+
availability_zone: "#{config.region}#{az_suffix}",
|
|
60
|
+
map_public_ip_on_launch: true,
|
|
61
|
+
tags: tags.merge(Name: "#{name}-subnet-#{az_suffix}")
|
|
62
|
+
)
|
|
63
|
+
network.add_subnet(:"subnet_#{az_suffix}", subnet)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
network
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Create IAM role for the EKS cluster and node groups
|
|
70
|
+
def create_iam(ctx, name, config, tags)
|
|
71
|
+
iam = Architecture::AwsEksIamResult.new
|
|
72
|
+
|
|
73
|
+
# Cluster role — use provided role_arn or create one
|
|
74
|
+
unless config.role_arn
|
|
75
|
+
assume_role_policy = {
|
|
76
|
+
Version: '2012-10-17',
|
|
77
|
+
Statement: [{
|
|
78
|
+
Effect: 'Allow',
|
|
79
|
+
Principal: { Service: 'eks.amazonaws.com' },
|
|
80
|
+
Action: 'sts:AssumeRole'
|
|
81
|
+
}]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
iam.cluster_role = ctx.aws_iam_role(
|
|
85
|
+
:"#{name}_cluster_role",
|
|
86
|
+
name: "#{name}-eks-cluster-role",
|
|
87
|
+
assume_role_policy: assume_role_policy,
|
|
88
|
+
tags: tags.merge(Name: "#{name}-cluster-role")
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
iam.cluster_policy_attachment = ctx.aws_iam_role_policy_attachment(
|
|
92
|
+
:"#{name}_cluster_policy",
|
|
93
|
+
role: iam.cluster_role.ref(:name),
|
|
94
|
+
policy_arn: 'arn:aws:iam::aws:policy/AmazonEKSClusterPolicy'
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Node role
|
|
99
|
+
node_assume_role_policy = {
|
|
100
|
+
Version: '2012-10-17',
|
|
101
|
+
Statement: [{
|
|
102
|
+
Effect: 'Allow',
|
|
103
|
+
Principal: { Service: 'ec2.amazonaws.com' },
|
|
104
|
+
Action: 'sts:AssumeRole'
|
|
105
|
+
}]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
iam.node_role = ctx.aws_iam_role(
|
|
109
|
+
:"#{name}_node_role",
|
|
110
|
+
name: "#{name}-eks-node-role",
|
|
111
|
+
assume_role_policy: node_assume_role_policy,
|
|
112
|
+
tags: tags.merge(Name: "#{name}-node-role")
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
%w[AmazonEKSWorkerNodePolicy AmazonEKS_CNI_Policy AmazonEC2ContainerRegistryReadOnly].each do |policy|
|
|
116
|
+
ctx.aws_iam_role_policy_attachment(
|
|
117
|
+
:"#{name}_node_#{policy.downcase.gsub(/[^a-z0-9]/, '_')}",
|
|
118
|
+
role: iam.node_role.ref(:name),
|
|
119
|
+
policy_arn: "arn:aws:iam::aws:policy/#{policy}"
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
iam
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Create the EKS cluster
|
|
127
|
+
def create_cluster(ctx, name, config, result, tags)
|
|
128
|
+
# Determine subnet IDs
|
|
129
|
+
subnet_ids = if config.network&.subnet_ids&.any?
|
|
130
|
+
config.network.subnet_ids
|
|
131
|
+
elsif result.network
|
|
132
|
+
if result.network.respond_to?(:subnet_ids)
|
|
133
|
+
result.network.subnet_ids
|
|
134
|
+
else
|
|
135
|
+
result.network.select { |k, _| k.to_s.start_with?('subnet_') }.values.map(&:id)
|
|
136
|
+
end
|
|
137
|
+
else
|
|
138
|
+
[]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Determine role ARN
|
|
142
|
+
role_arn = config.role_arn || result.iam&.dig(:cluster_role)&.arn
|
|
143
|
+
|
|
144
|
+
cluster_attrs = {
|
|
145
|
+
name: "#{name}-cluster",
|
|
146
|
+
role_arn: role_arn,
|
|
147
|
+
version: config.kubernetes_version,
|
|
148
|
+
vpc_config: {
|
|
149
|
+
subnet_ids: subnet_ids,
|
|
150
|
+
endpoint_private_access: config.network&.private_endpoint || true,
|
|
151
|
+
endpoint_public_access: config.network&.public_endpoint || false,
|
|
152
|
+
security_group_ids: config.network&.security_group_ids || []
|
|
153
|
+
},
|
|
154
|
+
tags: tags.merge(Name: "#{name}-cluster")
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
cluster_attrs[:enabled_cluster_log_types] = config.logging if config.logging.any?
|
|
158
|
+
|
|
159
|
+
if config.encryption_at_rest
|
|
160
|
+
cluster_attrs[:encryption_config] = {
|
|
161
|
+
resources: ['secrets']
|
|
162
|
+
}
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
ctx.aws_eks_cluster(:"#{name}_cluster", cluster_attrs)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Create an EKS managed node group
|
|
169
|
+
def create_node_pool(ctx, name, cluster_ref, pool_config, tags)
|
|
170
|
+
pool_name = :"#{name}_#{pool_config.name}"
|
|
171
|
+
|
|
172
|
+
node_group_attrs = {
|
|
173
|
+
cluster_name: cluster_ref.ref(:name),
|
|
174
|
+
node_group_name: "#{name}-#{pool_config.name}",
|
|
175
|
+
node_role_arn: "${aws_iam_role.#{name}_node_role.arn}",
|
|
176
|
+
instance_types: pool_config.instance_types,
|
|
177
|
+
scaling_config: {
|
|
178
|
+
desired_size: pool_config.effective_desired_size,
|
|
179
|
+
min_size: pool_config.min_size,
|
|
180
|
+
max_size: pool_config.max_size
|
|
181
|
+
},
|
|
182
|
+
disk_size: pool_config.disk_size_gb,
|
|
183
|
+
tags: tags.merge(
|
|
184
|
+
Name: "#{name}-#{pool_config.name}",
|
|
185
|
+
NodePool: pool_config.name.to_s
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
node_group_attrs[:labels] = pool_config.labels if pool_config.labels.any?
|
|
190
|
+
|
|
191
|
+
if pool_config.taints.any?
|
|
192
|
+
node_group_attrs[:taint] = pool_config.taints.map do |t|
|
|
193
|
+
{ key: t[:key], value: t[:value], effect: t[:effect] }
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
ctx.aws_eks_node_group(pool_name, node_group_attrs)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|