Module: Pangea::Kubernetes::Backends::NixosBase

Included in:
AwsNixos, AzureNixos, GcpNixos, HcloudK3s
Defined in:
lib/pangea/kubernetes/backends/nixos_base.rb

Overview

Template method module for NixOS backends. Extracts shared logic for all 4 NixOS backends (AWS, GCP, Azure, Hetzner).

Shared methods (implemented here):

- create_cluster: firewall + control plane server loop + cloud-init
- create_node_pool: worker cloud-init + scaling group delegation
- build_server_cloud_init: full option passthrough from config.nixos
- build_agent_cloud_init: worker cloud-init with join_server
- base_firewall_ports: cloud-agnostic port definitions
- build_secrets_hash: extracts path references from config

Template hooks (subclasses implement):

- create_compute_instance(ctx, resource_name, config, result, cloud_init, index, tags)
- create_worker_pool(ctx, name, cluster_ref, pool_config, cloud_init, tags)
- create_firewall_resources(ctx, name, config, network_result, tags)
- resolve_image(config)
- post_create_instance(ctx, name, server, result, index, tags)

Constant Summary collapse

COMMON_PORTS =

Kubernetes port definitions shared across all NixOS backends

{
  ssh: { port: 22, protocol: :tcp, public: true, description: 'SSH' },
  http: { port: 80, protocol: :tcp, public: true, description: 'HTTP' },
  https: { port: 443, protocol: :tcp, public: true, description: 'HTTPS' },
  api: { port: 6443, protocol: :tcp, public: true, description: 'K8s API' },
  kubelet: { port: 10_250, protocol: :tcp, public: false, description: 'Kubelet' },
  etcd: { port: '2379-2380', protocol: :tcp, public: false, description: 'etcd' },
  vxlan: { port: 8472, protocol: :udp, public: false, description: 'VXLAN' },
  wireguard: { port: 51_820, protocol: :udp, public: false, description: 'WireGuard VPN' }
}.freeze
VANILLA_K8S_PORTS =

Additional ports for vanilla Kubernetes

{
  controller_manager: { port: 10_257, protocol: :tcp, public: false, description: 'controller-manager' },
  scheduler: { port: 10_259, protocol: :tcp, public: false, description: 'scheduler' }
}.freeze
JOIN_SERVER_PLACEHOLDER =

Placeholder used in agent cloud-init for the join server address. Backends that defer user_data encoding to Terraform (e.g., AWS with terraform_base64encode) replace this with the actual Terraform expression at synthesis time via replace().

'__PANGEA_JOIN_SERVER__'
AGENT_BOOTSTRAP_KEYS =

Extract only the secrets workers need to join the cluster. Workers receive:

  • k3s_server_token: cluster join authentication

  • nix_github_token: access private flake inputs during nixos-rebuild

They do NOT receive flux tokens, SOPS keys, VPN keys, or admin passwords.

%i[k3s_server_token nix_github_token].freeze

Instance Method Summary collapse

Instance Method Details

#base_firewall_ports(distribution) ⇒ Object

Returns all firewall ports for the given distribution



59
60
61
62
63
# File 'lib/pangea/kubernetes/backends/nixos_base.rb', line 59

def base_firewall_ports(distribution)
  ports = COMMON_PORTS.dup
  ports.merge!(VANILLA_K8S_PORTS) if distribution.to_sym == :kubernetes
  ports
end

#build_agent_bootstrap_secrets(config) ⇒ Object



206
207
208
209
210
211
212
213
214
# File 'lib/pangea/kubernetes/backends/nixos_base.rb', line 206

def build_agent_bootstrap_secrets(config)
  bs = config.bootstrap_secrets
  return nil unless bs.is_a?(Hash)

  agent_secrets = AGENT_BOOTSTRAP_KEYS.each_with_object({}) do |key, h|
    h[key] = bs[key] if bs[key]
  end
  agent_secrets.empty? ? nil : agent_secrets
end

#build_agent_cloud_init(name, tags, cluster_ref, node_index: 'dynamic', use_join_placeholder: false) ⇒ Object

Build cloud-init for a worker/agent node. Workers receive only the k3s_server_token from bootstrap_secrets (they need it to authenticate to the control plane for cluster join).

node_index defaults to ‘dynamic’ — the generated shell script queries EC2 instance metadata at boot time to derive a unique index from the instance ID. This prevents duplicate hostnames when multiple ASG instances share the same launch template. Backends that create individual resources (e.g., Hetzner) can override with a static index.

When use_join_placeholder is true, the join_server value is replaced with JOIN_SERVER_PLACEHOLDER. This allows backends that use Terraform functions (e.g., base64encode) to inject the actual Terraform expression via replace() at apply time, avoiding premature encoding of $… references by Ruby.



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/pangea/kubernetes/backends/nixos_base.rb', line 139

def build_agent_cloud_init(name, tags, cluster_ref, node_index: 'dynamic', use_join_placeholder: false)
  track = if cluster_ref.respond_to?(:distribution_track) && cluster_ref.distribution_track
            cluster_ref.distribution_track
          else
            tags[:DistributionTrack] || '1.34'
          end

  agent_secrets = if cluster_ref.respond_to?(:agent_bootstrap_secrets)
                   cluster_ref.agent_bootstrap_secrets
                 end

  join_server = use_join_placeholder ? JOIN_SERVER_PLACEHOLDER : cluster_ref.ipv4_address

  BareMetal::CloudInit.generate(
    cluster_name: name.to_s,
    distribution: tags[:Distribution]&.to_sym || :k3s,
    profile: tags[:Profile] || 'cloud-server',
    distribution_track: track,
    role: 'agent',
    node_index: node_index,
    cluster_init: false,
    join_server: join_server,
    bootstrap_secrets: agent_secrets
  )
end

#build_bootstrap_secrets(config) ⇒ Object

Extract bootstrap secrets from config for cloud-init delivery. These are written to disk at first boot before sops-nix activates. Returns nil when no bootstrap secrets are configured.



191
192
193
194
195
196
197
# File 'lib/pangea/kubernetes/backends/nixos_base.rb', line 191

def build_bootstrap_secrets(config)
  bs = config.bootstrap_secrets
  return nil unless bs.is_a?(Hash) && bs.any?
  return nil if bs.values.all? { |v| v.nil? || (v.is_a?(String) && v.empty?) }

  bs
end

#build_secrets_hash(config) ⇒ Object

Extract secrets path references from config. Returns nil when no secrets are configured.



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/pangea/kubernetes/backends/nixos_base.rb', line 167

def build_secrets_hash(config)
  paths = {}

  if config.fluxcd
    paths[:flux_ssh_key_path] = config.fluxcd.source_ssh_key_file if config.fluxcd.source_ssh_key_file
    paths[:flux_token_path] = config.fluxcd.source_token_file if config.fluxcd.source_token_file
    paths[:sops_age_key_path] = config.fluxcd.sops_age_key_file if config.fluxcd.sops_age_key_file
  end

  if config.nixos&.secrets
    secrets = config.nixos.secrets
    paths[:flux_ssh_key_path] ||= secrets.flux_ssh_key_path if secrets.flux_ssh_key_path
    paths[:flux_token_path] ||= secrets.flux_token_path if secrets.flux_token_path
    paths[:sops_age_key_path] ||= secrets.sops_age_key_path if secrets.sops_age_key_path
    paths[:join_token_path] = secrets.join_token_path if secrets.join_token_path
    paths.merge!(secrets.extra_paths) if secrets.extra_paths.any?
  end

  paths.empty? ? nil : paths
end

#build_server_cloud_init(name, config, index, result) ⇒ Object

Build cloud-init for a control plane server with full option passthrough.



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/pangea/kubernetes/backends/nixos_base.rb', line 98

def build_server_cloud_init(name, config, index, result)
  gitops_config = case config.gitops_operator
                  when :fluxcd then config.fluxcd&.to_h
                  when :argocd then config.argocd&.to_h
                  end

  BareMetal::CloudInit.generate(
    cluster_name: name.to_s,
    distribution: config.distribution,
    profile: config.profile,
    distribution_track: config.distribution_track || config.kubernetes_version,
    role: 'server',
    node_index: index,
    cluster_init: index.zero?,
    network_id: result.network&.dig(:network)&.id,
    fluxcd: config.gitops_operator == :fluxcd ? gitops_config : nil,
    argocd: config.gitops_operator == :argocd ? gitops_config : nil,
    k3s: config.distribution == :k3s ? config.nixos&.k3s&.to_h : nil,
    kubernetes: config.distribution == :kubernetes ? config.nixos&.kubernetes&.to_h : nil,
    secrets: build_secrets_hash(config),
    vpn: config.vpn&.to_h,
    bootstrap_secrets: build_bootstrap_secrets(config),
    persistent_state: config.persistent_state&.to_h
  )
end

#create_compute_instance(_ctx, _name, _config, _result, _cloud_init, _index, _tags) ⇒ Object

Create a single compute instance. Returns a resource reference.

Raises:

  • (NotImplementedError)


219
220
221
# File 'lib/pangea/kubernetes/backends/nixos_base.rb', line 219

def create_compute_instance(_ctx, _name, _config, _result, _cloud_init, _index, _tags)
  raise NotImplementedError, "#{self} must implement create_compute_instance"
end

#create_worker_pool(_ctx, _name, _cluster_ref, _pool_config, _cloud_init, _tags) ⇒ Object

Create a worker pool (ASG, MIG, VMSS, or server loop). Returns a resource reference.

Raises:

  • (NotImplementedError)


224
225
226
# File 'lib/pangea/kubernetes/backends/nixos_base.rb', line 224

def create_worker_pool(_ctx, _name, _cluster_ref, _pool_config, _cloud_init, _tags)
  raise NotImplementedError, "#{self} must implement create_worker_pool"
end

#nixos_create_cluster(ctx, name, config, result, tags) ⇒ Object

Create control plane server(s) via template hooks. Subclasses override create_compute_instance and create_firewall_resources.



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/pangea/kubernetes/backends/nixos_base.rb', line 67

def nixos_create_cluster(ctx, name, config, result, tags)
  system_pool = config.system_node_pool
  cp_count = [system_pool.min_size, 1].max
  servers = []

  cp_count.times do |idx|
    cloud_init = build_server_cloud_init(name, config, idx, result)

    server = create_compute_instance(ctx, name, config, result, cloud_init, idx, tags)
    post_create_instance(ctx, name, server, result, idx, tags)

    servers << server
  end

  servers.first
end

#nixos_create_node_pool(ctx, name, cluster_ref, pool_config, tags) ⇒ Object

Create worker node pool via template hooks. Subclasses override create_worker_pool.



92
93
94
95
# File 'lib/pangea/kubernetes/backends/nixos_base.rb', line 92

def nixos_create_node_pool(ctx, name, cluster_ref, pool_config, tags)
  cloud_init = build_agent_cloud_init(name, tags, cluster_ref)
  create_worker_pool(ctx, name, cluster_ref, pool_config, cloud_init, tags)
end

#post_create_instance(_ctx, _name, _server, _result, _index, _tags) ⇒ Object

Post-instance creation hook (e.g., Hetzner network attachment). No-op by default.



229
230
231
# File 'lib/pangea/kubernetes/backends/nixos_base.rb', line 229

def post_create_instance(_ctx, _name, _server, _result, _index, _tags)
  # no-op — subclasses override when needed
end