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,176 @@
|
|
|
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
|
+
# GCP GKE backend — creates managed GKE clusters with VPC-native networking.
|
|
23
|
+
module GcpGke
|
|
24
|
+
include Base
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
def backend_name = :gcp
|
|
28
|
+
def managed_kubernetes? = true
|
|
29
|
+
def required_gem = 'pangea-gcp'
|
|
30
|
+
|
|
31
|
+
def load_provider!
|
|
32
|
+
require required_gem
|
|
33
|
+
rescue LoadError => e
|
|
34
|
+
raise LoadError,
|
|
35
|
+
"Backend :gcp requires gem 'pangea-gcp'. " \
|
|
36
|
+
"Add it to your Gemfile: gem 'pangea-gcp'\n" \
|
|
37
|
+
"Original error: #{e.message}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Create GCP VPC network + subnet
|
|
41
|
+
def create_network(ctx, name, config, tags)
|
|
42
|
+
network = Architecture::NetworkResult.new
|
|
43
|
+
|
|
44
|
+
network.vpc = ctx.google_compute_network(
|
|
45
|
+
:"#{name}_network",
|
|
46
|
+
name: "#{name}-network",
|
|
47
|
+
auto_create_subnetworks: false,
|
|
48
|
+
project: config.project
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
subnet = ctx.google_compute_subnetwork(
|
|
52
|
+
:"#{name}_subnet",
|
|
53
|
+
name: "#{name}-subnet",
|
|
54
|
+
ip_cidr_range: config.network&.vpc_cidr || '10.0.0.0/20',
|
|
55
|
+
region: config.region,
|
|
56
|
+
network: network.vpc.id,
|
|
57
|
+
project: config.project,
|
|
58
|
+
secondary_ip_range: [
|
|
59
|
+
{ range_name: "#{name}-pods", ip_cidr_range: config.network&.pod_cidr || '10.1.0.0/16' },
|
|
60
|
+
{ range_name: "#{name}-services", ip_cidr_range: config.network&.service_cidr || '10.2.0.0/20' }
|
|
61
|
+
]
|
|
62
|
+
)
|
|
63
|
+
network.add_subnet(:subnet, subnet)
|
|
64
|
+
|
|
65
|
+
network
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# GKE uses Workload Identity — no standalone IAM resources needed
|
|
69
|
+
def create_iam(ctx, name, config, tags)
|
|
70
|
+
iam = Architecture::GcpIamResult.new
|
|
71
|
+
|
|
72
|
+
# Service account for GKE nodes
|
|
73
|
+
iam.node_sa = ctx.google_service_account(
|
|
74
|
+
:"#{name}_node_sa",
|
|
75
|
+
account_id: "#{name}-gke-nodes",
|
|
76
|
+
display_name: "#{name} GKE Node Service Account",
|
|
77
|
+
project: config.project
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Bind minimum required roles
|
|
81
|
+
%w[logging.logWriter monitoring.metricWriter monitoring.viewer].each do |role|
|
|
82
|
+
ctx.google_project_iam_member(
|
|
83
|
+
:"#{name}_node_#{role.gsub('.', '_')}",
|
|
84
|
+
project: config.project,
|
|
85
|
+
role: "roles/#{role}",
|
|
86
|
+
member: "serviceAccount:#{iam.node_sa.email}"
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
iam
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Create the GKE cluster
|
|
94
|
+
def create_cluster(ctx, name, config, result, tags)
|
|
95
|
+
cluster_attrs = {
|
|
96
|
+
name: "#{name}-cluster",
|
|
97
|
+
location: config.region,
|
|
98
|
+
project: config.project,
|
|
99
|
+
initial_node_count: 1,
|
|
100
|
+
remove_default_node_pool: true,
|
|
101
|
+
min_master_version: config.kubernetes_version,
|
|
102
|
+
deletion_protection: false,
|
|
103
|
+
networking_mode: 'VPC_NATIVE',
|
|
104
|
+
resource_labels: gke_labels(tags)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
# VPC-native networking
|
|
108
|
+
if result.network
|
|
109
|
+
cluster_attrs[:network] = result.network[:vpc]&.id
|
|
110
|
+
cluster_attrs[:subnetwork] = result.network[:subnet]&.id
|
|
111
|
+
cluster_attrs[:ip_allocation_policy] = {
|
|
112
|
+
cluster_secondary_range_name: "#{name}-pods",
|
|
113
|
+
services_secondary_range_name: "#{name}-services"
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Private cluster
|
|
118
|
+
if config.network&.private_endpoint
|
|
119
|
+
cluster_attrs[:private_cluster_config] = {
|
|
120
|
+
enable_private_nodes: true,
|
|
121
|
+
enable_private_endpoint: !config.network.public_endpoint,
|
|
122
|
+
master_ipv4_cidr_block: '172.16.0.0/28'
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Workload Identity
|
|
127
|
+
if config.project
|
|
128
|
+
cluster_attrs[:workload_identity_config] = {
|
|
129
|
+
workload_pool: "#{config.project}.svc.id.goog"
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Release channel
|
|
134
|
+
cluster_attrs[:release_channel] = { channel: 'REGULAR' }
|
|
135
|
+
|
|
136
|
+
ctx.google_container_cluster(:"#{name}_cluster", cluster_attrs)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Create a GKE node pool
|
|
140
|
+
def create_node_pool(ctx, name, cluster_ref, pool_config, tags)
|
|
141
|
+
pool_name = :"#{name}_#{pool_config.name}"
|
|
142
|
+
|
|
143
|
+
node_pool_attrs = {
|
|
144
|
+
name: "#{name}-#{pool_config.name}",
|
|
145
|
+
cluster: cluster_ref.id,
|
|
146
|
+
location: tags[:Region] || 'us-central1',
|
|
147
|
+
initial_node_count: pool_config.effective_desired_size,
|
|
148
|
+
node_config: {
|
|
149
|
+
machine_type: pool_config.instance_types.first,
|
|
150
|
+
disk_size_gb: pool_config.disk_size_gb,
|
|
151
|
+
oauth_scopes: ['https://www.googleapis.com/auth/cloud-platform'],
|
|
152
|
+
labels: pool_config.labels.merge(
|
|
153
|
+
'node-pool' => pool_config.name.to_s
|
|
154
|
+
)
|
|
155
|
+
},
|
|
156
|
+
autoscaling: {
|
|
157
|
+
min_node_count: pool_config.min_size,
|
|
158
|
+
max_node_count: pool_config.max_size
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
ctx.google_container_node_pool(pool_name, node_pool_attrs)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
private
|
|
166
|
+
|
|
167
|
+
# Convert tags to GKE-compatible labels (lowercase, hyphens)
|
|
168
|
+
def gke_labels(tags)
|
|
169
|
+
tags.transform_keys { |k| k.to_s.downcase.gsub(/[^a-z0-9-]/, '-') }
|
|
170
|
+
.transform_values { |v| v.to_s.downcase.gsub(/[^a-z0-9-]/, '-') }
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,240 @@
|
|
|
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
|
+
require 'pangea/kubernetes/backends/nixos_base'
|
|
19
|
+
|
|
20
|
+
module Pangea
|
|
21
|
+
module Kubernetes
|
|
22
|
+
module Backends
|
|
23
|
+
# GCP NixOS backend — GCE instances running NixOS with k3s/k8s
|
|
24
|
+
# via blackmatter-kubernetes modules.
|
|
25
|
+
#
|
|
26
|
+
# Uses:
|
|
27
|
+
# - GCE instances for control plane (static)
|
|
28
|
+
# - Managed Instance Groups (MIGs) for worker node pools
|
|
29
|
+
# - VPC + Firewall rules for networking
|
|
30
|
+
# - Instance Templates for NixOS image + cloud-init
|
|
31
|
+
#
|
|
32
|
+
# No managed K8s services (GKE) — all k3s/k8s managed by NixOS.
|
|
33
|
+
module GcpNixos
|
|
34
|
+
include Base
|
|
35
|
+
extend NixosBase
|
|
36
|
+
|
|
37
|
+
class << self
|
|
38
|
+
def backend_name = :gcp_nixos
|
|
39
|
+
def managed_kubernetes? = false
|
|
40
|
+
def required_gem = 'pangea-gcp'
|
|
41
|
+
|
|
42
|
+
def load_provider!
|
|
43
|
+
require required_gem
|
|
44
|
+
rescue LoadError => e
|
|
45
|
+
raise LoadError,
|
|
46
|
+
"Backend :gcp_nixos requires gem 'pangea-gcp'. " \
|
|
47
|
+
"Add it to your Gemfile: gem 'pangea-gcp'\n" \
|
|
48
|
+
"Original error: #{e.message}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Create VPC network + subnet + firewall rules
|
|
52
|
+
def create_network(ctx, name, config, tags)
|
|
53
|
+
network = Architecture::GcpNetworkResult.new
|
|
54
|
+
|
|
55
|
+
network.vpc = ctx.google_compute_network(
|
|
56
|
+
:"#{name}_network",
|
|
57
|
+
name: "#{name}-network",
|
|
58
|
+
auto_create_subnetworks: false,
|
|
59
|
+
project: config.project
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
subnet = ctx.google_compute_subnetwork(
|
|
63
|
+
:"#{name}_subnet",
|
|
64
|
+
name: "#{name}-subnet",
|
|
65
|
+
ip_cidr_range: config.network&.vpc_cidr || '10.0.0.0/20',
|
|
66
|
+
region: config.region,
|
|
67
|
+
network: network.vpc.id,
|
|
68
|
+
project: config.project
|
|
69
|
+
)
|
|
70
|
+
network.add_subnet(:subnet, subnet)
|
|
71
|
+
|
|
72
|
+
# Firewall rules for k3s/k8s
|
|
73
|
+
network.firewall_internal = ctx.google_compute_firewall(
|
|
74
|
+
:"#{name}_fw_internal",
|
|
75
|
+
name: "#{name}-allow-internal",
|
|
76
|
+
network: network.vpc.id,
|
|
77
|
+
project: config.project,
|
|
78
|
+
allow: [
|
|
79
|
+
{ protocol: 'tcp', ports: %w[0-65535] },
|
|
80
|
+
{ protocol: 'udp', ports: %w[0-65535] },
|
|
81
|
+
{ protocol: 'icmp' }
|
|
82
|
+
],
|
|
83
|
+
source_ranges: [config.network&.vpc_cidr || '10.0.0.0/20']
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
network.firewall_external = ctx.google_compute_firewall(
|
|
87
|
+
:"#{name}_fw_external",
|
|
88
|
+
name: "#{name}-allow-external",
|
|
89
|
+
network: network.vpc.id,
|
|
90
|
+
project: config.project,
|
|
91
|
+
allow: [
|
|
92
|
+
{ protocol: 'tcp', ports: %w[22 80 443 6443] }
|
|
93
|
+
],
|
|
94
|
+
source_ranges: ['0.0.0.0/0']
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
network
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Service account for GCE instances (minimal permissions)
|
|
101
|
+
def create_iam(ctx, name, config, tags)
|
|
102
|
+
iam = Architecture::GcpIamResult.new
|
|
103
|
+
|
|
104
|
+
iam.node_sa = ctx.google_service_account(
|
|
105
|
+
:"#{name}_node_sa",
|
|
106
|
+
account_id: "#{name}-nixos-nodes",
|
|
107
|
+
display_name: "#{name} NixOS K8s Node Service Account",
|
|
108
|
+
project: config.project
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
%w[logging.logWriter monitoring.metricWriter].each do |role|
|
|
112
|
+
ctx.google_project_iam_member(
|
|
113
|
+
:"#{name}_node_#{role.gsub('.', '_')}",
|
|
114
|
+
project: config.project,
|
|
115
|
+
role: "roles/#{role}",
|
|
116
|
+
member: "serviceAccount:#{iam.node_sa.email}"
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
iam
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Create control plane GCE instances (static, no MIG)
|
|
124
|
+
def create_cluster(ctx, name, config, result, tags)
|
|
125
|
+
nixos_create_cluster(ctx, name, config, result, tags)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Create worker node pool via Instance Template + MIG + Autoscaler
|
|
129
|
+
def create_node_pool(ctx, name, cluster_ref, pool_config, tags)
|
|
130
|
+
nixos_create_node_pool(ctx, name, cluster_ref, pool_config, tags)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# --- NixosBase template hooks ---
|
|
134
|
+
|
|
135
|
+
def create_compute_instance(ctx, name, config, result, cloud_init, index, tags)
|
|
136
|
+
system_pool = config.system_node_pool
|
|
137
|
+
machine_type = system_pool.instance_types.first
|
|
138
|
+
image = config.gce_image || config.nixos&.image_id || 'nixos-24-05'
|
|
139
|
+
|
|
140
|
+
ctx.google_compute_instance(
|
|
141
|
+
:"#{name}_cp_#{index}",
|
|
142
|
+
name: "#{name}-cp-#{index}",
|
|
143
|
+
machine_type: machine_type,
|
|
144
|
+
zone: "#{config.region}-a",
|
|
145
|
+
project: config.project,
|
|
146
|
+
boot_disk: {
|
|
147
|
+
initialize_params: {
|
|
148
|
+
image: image,
|
|
149
|
+
size: system_pool.disk_size_gb
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
network_interface: {
|
|
153
|
+
network: result.network&.dig(:vpc)&.id,
|
|
154
|
+
subnetwork: result.network&.dig(:subnet)&.id,
|
|
155
|
+
access_config: {}
|
|
156
|
+
},
|
|
157
|
+
metadata: {
|
|
158
|
+
'user-data' => cloud_init
|
|
159
|
+
},
|
|
160
|
+
service_account: {
|
|
161
|
+
email: result.iam&.dig(:node_sa)&.email,
|
|
162
|
+
scopes: ['cloud-platform']
|
|
163
|
+
},
|
|
164
|
+
labels: gce_labels(tags.merge(
|
|
165
|
+
role: 'control-plane',
|
|
166
|
+
node_index: index.to_s,
|
|
167
|
+
distribution: config.distribution.to_s
|
|
168
|
+
))
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def create_worker_pool(ctx, name, _cluster_ref, pool_config, cloud_init, tags)
|
|
173
|
+
pool_name = :"#{name}_#{pool_config.name}"
|
|
174
|
+
machine_type = pool_config.instance_types.first
|
|
175
|
+
|
|
176
|
+
# Instance Template
|
|
177
|
+
template = ctx.google_compute_instance_template(
|
|
178
|
+
:"#{pool_name}_template",
|
|
179
|
+
name: "#{name}-#{pool_config.name}-template",
|
|
180
|
+
machine_type: machine_type,
|
|
181
|
+
project: tags[:Project],
|
|
182
|
+
disk: [{
|
|
183
|
+
source_image: tags[:Image] || 'nixos-24-05',
|
|
184
|
+
disk_size_gb: pool_config.disk_size_gb,
|
|
185
|
+
auto_delete: true,
|
|
186
|
+
boot: true
|
|
187
|
+
}],
|
|
188
|
+
network_interface: {
|
|
189
|
+
network: tags[:NetworkId],
|
|
190
|
+
subnetwork: tags[:SubnetId],
|
|
191
|
+
access_config: {}
|
|
192
|
+
},
|
|
193
|
+
metadata: { 'user-data' => cloud_init },
|
|
194
|
+
labels: gce_labels(tags.merge(
|
|
195
|
+
role: 'worker',
|
|
196
|
+
node_pool: pool_config.name.to_s
|
|
197
|
+
))
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Managed Instance Group
|
|
201
|
+
mig = ctx.google_compute_instance_group_manager(
|
|
202
|
+
:"#{pool_name}_mig",
|
|
203
|
+
name: "#{name}-#{pool_config.name}-mig",
|
|
204
|
+
base_instance_name: "#{name}-#{pool_config.name}",
|
|
205
|
+
zone: "#{tags[:Region] || 'us-central1'}-a",
|
|
206
|
+
project: tags[:Project],
|
|
207
|
+
target_size: pool_config.effective_desired_size,
|
|
208
|
+
version: [{
|
|
209
|
+
instance_template: template.id
|
|
210
|
+
}]
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Autoscaler
|
|
214
|
+
ctx.google_compute_autoscaler(
|
|
215
|
+
:"#{pool_name}_autoscaler",
|
|
216
|
+
name: "#{name}-#{pool_config.name}-autoscaler",
|
|
217
|
+
zone: "#{tags[:Region] || 'us-central1'}-a",
|
|
218
|
+
project: tags[:Project],
|
|
219
|
+
target: mig.id,
|
|
220
|
+
autoscaling_policy: {
|
|
221
|
+
min_replicas: pool_config.min_size,
|
|
222
|
+
max_replicas: pool_config.max_size,
|
|
223
|
+
cpu_utilization: { target: 0.7 }
|
|
224
|
+
}
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
mig
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
private
|
|
231
|
+
|
|
232
|
+
def gce_labels(tags)
|
|
233
|
+
tags.transform_keys { |k| k.to_s.downcase.gsub(/[^a-z0-9-]/, '-') }
|
|
234
|
+
.transform_values { |v| v.to_s.downcase.gsub(/[^a-z0-9-]/, '-') }
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
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
|
+
require 'pangea/kubernetes/backends/nixos_base'
|
|
19
|
+
|
|
20
|
+
module Pangea
|
|
21
|
+
module Kubernetes
|
|
22
|
+
module Backends
|
|
23
|
+
# Hetzner Cloud NixOS backend.
|
|
24
|
+
# Provisions NixOS VMs with cloud-init carrying blackmatter-kubernetes config.
|
|
25
|
+
# Supports both k3s and vanilla Kubernetes distributions.
|
|
26
|
+
module HcloudK3s
|
|
27
|
+
include Base
|
|
28
|
+
extend NixosBase
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
def backend_name = :hcloud
|
|
32
|
+
def managed_kubernetes? = false
|
|
33
|
+
def required_gem = 'pangea-hcloud'
|
|
34
|
+
|
|
35
|
+
def load_provider!
|
|
36
|
+
require required_gem
|
|
37
|
+
rescue LoadError => e
|
|
38
|
+
raise LoadError,
|
|
39
|
+
"Backend :hcloud requires gem 'pangea-hcloud'. " \
|
|
40
|
+
"Add it to your Gemfile: gem 'pangea-hcloud'\n" \
|
|
41
|
+
"Original error: #{e.message}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Create Hetzner Cloud network + subnet
|
|
45
|
+
def create_network(ctx, name, config, tags)
|
|
46
|
+
network = Architecture::HcloudNetworkResult.new
|
|
47
|
+
|
|
48
|
+
ip_range = config.network&.vpc_cidr || '10.0.0.0/16'
|
|
49
|
+
network.network = ctx.hcloud_network(
|
|
50
|
+
:"#{name}_network",
|
|
51
|
+
name: "#{name}-network",
|
|
52
|
+
ip_range: ip_range,
|
|
53
|
+
labels: hcloud_labels(tags)
|
|
54
|
+
)
|
|
55
|
+
network.vpc = network.network
|
|
56
|
+
|
|
57
|
+
subnet = ctx.hcloud_network_subnet(
|
|
58
|
+
:"#{name}_subnet",
|
|
59
|
+
network_id: network.network.id,
|
|
60
|
+
type: 'cloud',
|
|
61
|
+
network_zone: config.region,
|
|
62
|
+
ip_range: config.network&.pod_cidr || '10.0.1.0/24'
|
|
63
|
+
)
|
|
64
|
+
network.add_subnet(:subnet, subnet)
|
|
65
|
+
|
|
66
|
+
network
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# NixOS doesn't use cloud IAM — return empty
|
|
70
|
+
def create_iam(_ctx, _name, _config, _tags)
|
|
71
|
+
Architecture::IamResult.new
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Create control plane server(s) as hcloud_server resources
|
|
75
|
+
def create_cluster(ctx, name, config, result, tags)
|
|
76
|
+
# Create firewall first
|
|
77
|
+
ctx.hcloud_firewall(
|
|
78
|
+
:"#{name}_firewall",
|
|
79
|
+
name: "#{name}-firewall",
|
|
80
|
+
rules: hcloud_firewall_rules(config.distribution),
|
|
81
|
+
labels: hcloud_labels(tags)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
nixos_create_cluster(ctx, name, config, result, tags)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Create worker nodes as hcloud_server resources
|
|
88
|
+
def create_node_pool(ctx, name, cluster_ref, pool_config, tags)
|
|
89
|
+
# Hetzner doesn't have ASG — create individual servers
|
|
90
|
+
server_type = pool_config.instance_types.first
|
|
91
|
+
count = pool_config.effective_desired_size
|
|
92
|
+
|
|
93
|
+
servers = []
|
|
94
|
+
count.times do |idx|
|
|
95
|
+
# Generate per-worker cloud-init with unique node_index (not shared)
|
|
96
|
+
cloud_init = build_agent_cloud_init(name, tags, cluster_ref, node_index: idx)
|
|
97
|
+
|
|
98
|
+
server = ctx.hcloud_server(
|
|
99
|
+
:"#{name}_#{pool_config.name}_#{idx}",
|
|
100
|
+
name: "#{name}-#{pool_config.name}-#{idx}",
|
|
101
|
+
server_type: server_type,
|
|
102
|
+
image: 'ubuntu-24.04',
|
|
103
|
+
location: tags[:Region] || 'nbg1',
|
|
104
|
+
user_data: cloud_init,
|
|
105
|
+
ssh_keys: pool_config.ssh_keys,
|
|
106
|
+
labels: hcloud_labels(tags.merge(
|
|
107
|
+
Role: 'worker',
|
|
108
|
+
NodePool: pool_config.name.to_s,
|
|
109
|
+
NodeIndex: idx.to_s
|
|
110
|
+
))
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
servers << server
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
servers.first
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# --- NixosBase template hooks ---
|
|
120
|
+
|
|
121
|
+
def create_compute_instance(ctx, name, config, result, cloud_init, index, tags)
|
|
122
|
+
system_pool = config.system_node_pool
|
|
123
|
+
server_type = system_pool.instance_types.first
|
|
124
|
+
firewall = result.network ? ctx.created_resources&.find { |r| r[:type] == 'hcloud_firewall' } : nil
|
|
125
|
+
|
|
126
|
+
ctx.hcloud_server(
|
|
127
|
+
:"#{name}_cp_#{index}",
|
|
128
|
+
name: "#{name}-cp-#{index}",
|
|
129
|
+
server_type: server_type,
|
|
130
|
+
image: nixos_image(config),
|
|
131
|
+
location: config.region,
|
|
132
|
+
user_data: cloud_init,
|
|
133
|
+
ssh_keys: system_pool.ssh_keys,
|
|
134
|
+
firewall_ids: firewall ? [firewall[:ref].id] : [],
|
|
135
|
+
labels: hcloud_labels(tags.merge(
|
|
136
|
+
Role: 'control-plane',
|
|
137
|
+
NodeIndex: index.to_s,
|
|
138
|
+
Distribution: config.distribution.to_s
|
|
139
|
+
))
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Override post_create_instance for Hetzner network attachment
|
|
144
|
+
def post_create_instance(ctx, name, server, result, index, _tags)
|
|
145
|
+
return unless result.network&.dig(:network)
|
|
146
|
+
|
|
147
|
+
ctx.hcloud_server_network(
|
|
148
|
+
:"#{name}_cp_#{index}_network",
|
|
149
|
+
server_id: server.id,
|
|
150
|
+
network_id: result.network[:network].id
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
def nixos_image(config)
|
|
157
|
+
config.nixos&.image_id || 'ubuntu-24.04'
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Convert standard tags to Hetzner labels (lowercase, underscored)
|
|
161
|
+
def hcloud_labels(tags)
|
|
162
|
+
tags.transform_keys { |k| k.to_s.downcase.gsub(/[^a-z0-9_]/, '_') }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Firewall rules for k3s or vanilla k8s
|
|
166
|
+
def hcloud_firewall_rules(distribution)
|
|
167
|
+
base_firewall_ports(distribution).map do |_name, port_def|
|
|
168
|
+
source_ips = port_def[:public] ? ['0.0.0.0/0', '::/0'] : ['10.0.0.0/8']
|
|
169
|
+
{
|
|
170
|
+
direction: 'in',
|
|
171
|
+
protocol: port_def[:protocol].to_s,
|
|
172
|
+
port: port_def[:port].to_s,
|
|
173
|
+
source_ips: source_ips
|
|
174
|
+
}
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|