Documentation
¶
Index ¶
- func FirewallEnabled(firewall config.FirewallConfig) bool
- func NewHetznerCloudProvider() cloud.CloudProvider
- func WaitForDNSResolution(ctx context.Context, fqdns []string, expectedIP string) error
- func WaitForIngressLBDNS(ctx context.Context, clusterClient client.Client) error
- type ActivateHRobotLinuxInstallationResponseBody
- type CreateVSwitchResponseBody
- type FailoverIPDetails
- type FirewallRule
- type FirewallRuleset
- type GetFailoverIPDetailsResponse
- type GetKeysResponse
- type GetServerResponseBody
- type HCloudMachineTemplateUpdates
- type HRobotResetResponseBody
- type Hetzner
- func (h *Hetzner) AttachHCloudServerToNetwork(ctx context.Context, serverID, networkID int) error
- func (h *Hetzner) AttachServerToVSwitch(ctx context.Context, serverID string, vswitchID int) error
- func (h *Hetzner) ConnectVSwitchWithHetznerNetwork(ctx context.Context, network *hcloud.Network) error
- func (h *Hetzner) CreateHCloudSSHKey(ctx context.Context, name string, sshKeyPair config.SSHKeyPairConfig) error
- func (h *Hetzner) CreateHetznerBareMetalSSHKey(ctx context.Context, name string, sshKeyPair config.SSHKeyPairConfig) error
- func (h *Hetzner) CreateLB(ctx context.Context, clusterName string, network *hcloud.Network, ...) (*hcloud.LoadBalancer, error)
- func (h *Hetzner) CreateNATGateway(ctx context.Context, networkID int) error
- func (h *Hetzner) CreateNetwork(ctx context.Context) (*hcloud.Network, error)
- func (h *Hetzner) CreateVSwitch(ctx context.Context) (int, error)
- func (h *Hetzner) DisableControlPlaneLBPublicInterface(ctx context.Context) error
- func (h *Hetzner) DisableDeletionProtection(ctx context.Context) error
- func (h *Hetzner) EnsureRobotFirewall(ctx context.Context, serverIP string, desired FirewallRuleset) error
- func (h *Hetzner) GenerateStoragePlans(ctx context.Context, hetznerConfig *config.HetznerConfig) error
- func (h *Hetzner) GetHCloudServerIDsForCluster(ctx context.Context, name string) ([]int, error)
- func (h *Hetzner) GetHetznerBareMetalHostPublicIPs(ctx context.Context) (map[string]string, error)
- func (h *Hetzner) GetVMSpecs(ctx context.Context, machineType string) (*cloud.VMSpec, error)
- func (h *Hetzner) InstallOSOnAllHBMS(ctx context.Context) error
- func (h *Hetzner) PointFailoverIPToInitMasterNode(ctx context.Context) error
- func (h *Hetzner) ProvisionPrerequisiteInfrastructure(ctx context.Context) error
- func (h *Hetzner) SetControlPlaneLBPublicInterface(ctx context.Context, clusterName string, enabled bool) (*hcloud.LoadBalancer, error)
- func (*Hetzner) SetupDisasterRecovery(_ context.Context) error
- func (*Hetzner) UpdateCapiClusterValuesFile(ctx context.Context, path string, updates any) error
- func (*Hetzner) UpdateMachineTemplate(ctx context.Context, clusterClient client.Client, name string, updates any) error
- type HetznerBareMetalMachineTemplateUpdates
- type HetznerMachineTemplateUpdates
- type Key
- type ListVSwitchResponseBody
- type Server
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func FirewallEnabled ¶ added in v0.27.0
func FirewallEnabled(firewall config.FirewallConfig) bool
FirewallEnabled reports whether kubeaid-cli should manage the Robot firewall for the given bare-metal firewall config. Defaults to true; an explicit firewall.enabled:false opts out (e.g. a separate upstream L3 firewall fronts the cluster).
func NewHetznerCloudProvider ¶
func NewHetznerCloudProvider() cloud.CloudProvider
func WaitForDNSResolution ¶
WaitForDNSResolution blocks until every fqdn in fqdns resolves to expectedIP through the OS resolver, ctx is cancelled, or dnsTotalTimeout passes (the wait fails closed — interactive bootstrap can't loop forever on a missing record).
No skip option: bypassing the wait would just push the failure into a later stage (cert-manager's first ACME HTTP-01, NetBird OIDC callback, etc.) where the symptom is harder to diagnose. Better to fail here with a clear "DNS records still not resolving" + cache- flush hint than to half-bootstrap and debug from a downstream pod's 503.
func WaitForIngressLBDNS ¶
WaitForIngressLBDNS gates bootstrap on the operator pointing the public-facing FQDNs (keycloak.dns / netbird.dns / netbird.stunDNS / netbird.turnDNS) at the Traefik LB's public IP. Run as SyncAllArgoCDApps's beforeRemainingApps gate — after ccm + traefik are synced (so the LB is being provisioned) but before the application-layer apps (netbird, keycloakx) whose Ingress certificates depend on DNS resolving. We poll the Service for status.loadBalancer.ingress[0].ip and only then prompt the operator.
cert-manager's ACME challenges for keycloakx / netbird Ingresses retry with exponential backoff. Putting the DNS prompt here lets the next retry succeed instead of leaving the apps unhealthy while the operator scrambles to figure out which IP to point at.
No-op when no FQDNs are configured (workload clusters, or VPN clusters with no Keycloak/NetBird DNS set).
Types ¶
type FailoverIPDetails ¶
type FailoverIPDetails struct {
ActiveServerIP string `json:"active_server_ip"`
}
type FirewallRule ¶ added in v0.27.0
type FirewallRule struct {
Name string `json:"name"`
IPVersion string `json:"ip_version"`
Protocol string `json:"protocol,omitempty"`
DstPort string `json:"dst_port,omitempty"`
SrcIP string `json:"src_ip,omitempty"`
DstIP string `json:"dst_ip,omitempty"`
Action string `json:"action"`
}
FirewallRule models a single rule in a Hetzner Robot stateless firewall.
IPVersion must be "ipv4" or "ipv6". Protocol is "tcp", "udp", "icmp", or "" (any). DstPort is a port number or a "lo-hi" range string (e.g. "32768-65535"); "" for any. SrcIP and DstIP are CIDR strings; "" means any. Action is "accept" or "discard".
type FirewallRuleset ¶ added in v0.27.0
type FirewallRuleset struct {
Status string `json:"status"`
Rules []FirewallRule `json:"rules"`
}
FirewallRuleset is the complete set of rules for one direction on a Hetzner Robot per-server firewall.
Status is "active" or "disabled". Rules are evaluated top-to-bottom; the first match wins.
func ControlPlaneIngressRuleset ¶ added in v0.27.0
func ControlPlaneIngressRuleset(allowSSHFrom []string, allowPublic []config.FirewallPort, failoverIP string) FirewallRuleset
ControlPlaneIngressRuleset returns the inbound ruleset for a bare-metal control-plane node — the nodes that hold the failover IP, which is the cluster's single public ingress. See docs/hetzner-bare-metal-network-surface.md. Evaluated top-to-bottom, first match wins; anything unmatched is dropped by the firewall's implicit default-deny:
- SSH 22/tcp — from allowSSHFrom, else from all (see sshIngressRules)
- DENY 6443/tcp — kube-apiserver reached over the NetBird operator only
- ALLOW 80/443 — Traefik public ingress, scoped to failoverIP
- ALLOW allowPublic — extra public service ports (e.g. 5432/tcp), scoped to failoverIP
- ALLOW ICMP, ALLOW 32768-65535 return traffic
The public service ports (80/443 + allowPublic) are scoped to the failover IP because that is where public ingress lands — Traefik and any allowPublic service point at it — so a control-plane node's own main IP exposes nothing public but SSH. An empty failoverIP leaves them unscoped (any destination).
func WorkerIngressRuleset ¶ added in v0.27.0
func WorkerIngressRuleset(allowSSHFrom []string) FirewallRuleset
WorkerIngressRuleset returns the inbound ruleset for a bare-metal worker node. Workers serve no public traffic — it all enters via the control-plane failover IP — so the only public inbound is admin SSH (allowSSHFrom, else all) plus ICMP and stateless return traffic. Everything else (80/443, 6443, AllowPublic) is dropped by the firewall's implicit default-deny.
type GetFailoverIPDetailsResponse ¶
type GetFailoverIPDetailsResponse struct {
Failover FailoverIPDetails `json:"failover"`
}
type GetKeysResponse ¶
type GetKeysResponse []struct {
Key Key `json:"key"`
}
type GetServerResponseBody ¶
type GetServerResponseBody struct {
Server Server `json:"server"`
}
type HCloudMachineTemplateUpdates ¶
type HCloudMachineTemplateUpdates struct {
NewImageName string
}
type HRobotResetResponseBody ¶
type Hetzner ¶
type Hetzner struct {
// contains filtered or unexported fields
}
func NewRobotFirewallClient ¶ added in v0.27.0
NewRobotFirewallClient builds a Hetzner client wired only for Hetzner Robot firewall operations (EnsureRobotFirewall, with BareMetalIngressRuleset / DefaultBareMetalIngressRuleset). It needs nothing but Robot web-service credentials — no parsed cluster config, no management or workload cluster — so one-off tooling can reconcile a node's public-IP firewall directly against the Robot API. The same primitive backs the eventual in-CLI wiring.
func (*Hetzner) AttachHCloudServerToNetwork ¶
AttachHCloudServerToNetwork attaches the given HCloud server to the given Hetzner Network.
func (*Hetzner) AttachServerToVSwitch ¶
func (*Hetzner) ConnectVSwitchWithHetznerNetwork ¶
func (h *Hetzner) ConnectVSwitchWithHetznerNetwork(ctx context.Context, network *hcloud.Network) error
When using Hetzner Bare Metal, we need to establish private connectivity between them and the HCloud servers in a Hetzner Network, using a VSwitch.
func (*Hetzner) CreateHCloudSSHKey ¶
func (h *Hetzner) CreateHCloudSSHKey(ctx context.Context, name string, sshKeyPair config.SSHKeyPairConfig) error
CreateHCloudSSHKey creates the given SSH key in HCloud, if it doesn't already exist.
When an entry with the same fingerprint already exists under a different name (operator's personal yubikey re-registered as "<cluster>" via a prior run, or a hand-onboarded "ashish-laptop" entry), the existing entry is reused — Hetzner authenticates by key material, not label. The in-memory cfg.SSHKeyPair.Name is updated to match the existing entry so downstream readers (CAPH chart values, the SealedSecret, NAT-gateway server creation) all look up the right name.
func (*Hetzner) CreateHetznerBareMetalSSHKey ¶
func (h *Hetzner) CreateHetznerBareMetalSSHKey( ctx context.Context, name string, sshKeyPair config.SSHKeyPairConfig, ) error
CreateHetznerBareMetalSSHKey creates the given SSH key in Hetzner Bare Metal, if it doesn't already exist.
Same idempotency contract as CreateHCloudSSHKey: an entry with the same fingerprint under a different name (typical when the operator's SSH key has been hand-onboarded into Robot or registered by an earlier cluster) is reused, and cfg.SSHKeyPair.Name is updated to match so downstream chart values + SealedSecret render the right name.
func (*Hetzner) CreateLB ¶
func (h *Hetzner) CreateLB(ctx context.Context, clusterName string, network *hcloud.Network, location string, enablePublicInterface bool, ) (*hcloud.LoadBalancer, error)
CreateLB creates the Hetzner control-plane LB if it doesn't already exist. enablePublicInterface controls whether the LB carries a public IPv4 — true during bootstrap (NetBird isn't routing the private subnet yet), false in steady state.
func (*Hetzner) CreateNATGateway ¶
func (*Hetzner) CreateNetwork ¶
CreateNetwork creates the Hetzner Network, if it doesn't already exist.
func (*Hetzner) CreateVSwitch ¶
A VSwitch is used to establish private connectivity between Hetzner Bare Metal servers (and a Hetzner Network, when spinning up a Hetzner hybrid cluster). This function is responsible for creating that VSwitch, if it doesn't already exist. The VSwitch ID gets returned.
func (*Hetzner) DisableControlPlaneLBPublicInterface ¶
DisableControlPlaneLBPublicInterface is the post-bootstrap finalize call: clusters whose control-plane LB ran with a public interface during bootstrap (so kubeadm join, ArgoCD setup, etc. could reach the apiserver before NetBird was up) flip it off here, leaving the LB reachable only through its private IP via the NetBird mesh.
Applies to the two modes that pre-create the LB during bootstrap:
VPN clusters: this cluster IS the VPN, bootstraps behind a public LB and transitions to private-via-NetBird once netbird itself is Healthy. Until v1.x of this code the guard skipped these because HCloudVPNCluster is nil for the VPN itself, which left the control-plane public on a freshly bootstrapped VPN cluster — fixed by including cluster.type=vpn in the gate.
workload clusters connecting to an existing VPN: same public- during-join / private-after-NetBird lifecycle, signaled by a non-nil HCloudVPNCluster pointing at the parent VPN.
Other modes (bare-metal CP, plain workload without a parent VPN) never had a public HCloud LB to disable — early return.
Also gated on hostname being configured: without an FQDN the LB's public IP is the operator's only entry point to kube-apiserver, and disabling it would lock them out of their own cluster.
func (*Hetzner) DisableDeletionProtection ¶
DisableDeletionProtection disables deletion protection on the KubeAPI LB and NAT Gateway server so that CAPH can delete them during cluster teardown.
func (*Hetzner) EnsureRobotFirewall ¶ added in v0.27.0
func (h *Hetzner) EnsureRobotFirewall(ctx context.Context, serverIP string, desired FirewallRuleset) error
EnsureRobotFirewall idempotently reconciles the Hetzner Robot per-server stateless firewall for the given serverIP to the desired inbound ruleset.
Flow:
- GET /firewall/<serverIP> — fetch current state.
- Compare current inbound rules + status against desired.
- If equal, return nil (no-op).
- Otherwise POST /firewall/<serverIP> with desired rules encoded in Hetzner Robot bracket-notation form-data.
The method is intentionally not wired into any provisioning phase. Timing (pre- vs post-NetBird) is an unresolved team decision; wiring lives in a follow-up PR.
func (*Hetzner) GenerateStoragePlans ¶
func (*Hetzner) GetHCloudServerIDsForCluster ¶
GetHCloudServerIDsForCluster returns IDs of the HCloud servers associated with the given Kubernetes cluster which was provisioned using Cluster API Provider Hetzner (CAPH).
func (*Hetzner) GetHetznerBareMetalHostPublicIPs ¶ added in v0.26.0
GetHetznerBareMetalHostPublicIPs returns a map of HetznerBareMetalHost ServerID to the Robot main IP for that server. Used by the kubelet-csr- approver values template to widen providerIpPrefixes with one /32 per node (kubelet's serving-cert SAN list always contains every local interface IP — both vSwitch and public — so the approver's allow-list has to cover both or every CSR is denied).
Returns an empty map on non-bare-metal Hetzner setups; the matching values template guards on map presence and skips iteration. Robot API calls are sequential (small N, Robot's per-account rate limit is unforgiving).
Follow-up removal: once https://github.com/syself/cluster-api-provider-hetzner/issues/2095 lands and the CAPH dep is bumped to a version that includes the fix, the kubelet-csr-approver chart + this helper + its callers can all be deleted (CAPH's native CSR validator is structurally tighter than postfinance's IP-prefix allow-list).
func (*Hetzner) GetVMSpecs ¶
func (*Hetzner) InstallOSOnAllHBMS ¶
InstallOSOnAllHBMS installs Ubuntu on each HBMS in parallel. HBMS that are already SSH-reachable are skipped (idempotency). Since each HBMS's OS installation takes ~8-12 minutes regardless of others, processing them in parallel bounds total wall-clock time to that of the slowest single host.
func (*Hetzner) PointFailoverIPToInitMasterNode ¶
func (*Hetzner) ProvisionPrerequisiteInfrastructure ¶
ProvisionPrerequisiteInfrastructure provisions infrastructure required before CAPH starts spinning up the cluster.
func (*Hetzner) SetControlPlaneLBPublicInterface ¶
func (h *Hetzner) SetControlPlaneLBPublicInterface( ctx context.Context, clusterName string, enabled bool, ) (*hcloud.LoadBalancer, error)
SetControlPlaneLBPublicInterface ensures the LB's public interface matches enabled. Idempotent — no-op when already in the desired state. Used both during bootstrap (enable) and post-bootstrap finalize (disable) once NetBird routes the private endpoint.
func (*Hetzner) SetupDisasterRecovery ¶
func (*Hetzner) UpdateCapiClusterValuesFile ¶
type HetznerBareMetalMachineTemplateUpdates ¶
type HetznerBareMetalMachineTemplateUpdates struct {
NewImagePath string
}
type HetznerMachineTemplateUpdates ¶
type HetznerMachineTemplateUpdates struct {
HCloudMachineTemplateUpdates
HetznerBareMetalMachineTemplateUpdates
}