d3fb5dcd1a
Replace the ISC dhcpd PXE-boot VM with a Kea DHCP Kubernetes operator, modelled on bind-operator. The operator renders kea-dhcp4 config from CRs and runs an HA pair of kea-dhcp4 + kea-ctrl-agent servers behind an anycast Service. - add KeaCluster/KeaSubnet/KeaClientClass/KeaAPI CRDs (group kea.unkin.net) - render deterministic kea-dhcp4.conf + kea-ctrl-agent.conf into a ConfigMap and roll the StatefulSet via a config-hash annotation; best-effort hot-reload via the kea-ctrl-agent REST channel - run HA hot-standby (memfile leases) with stable per-peer DNS identity from a StatefulSet; expose an anycast LoadBalancer Service for PureLB - represent the full legacy dhcpd config: 198.18.13-17.0/24 pools, pool-less 198.18.25.0/24, and the Legacy/UEFI-64 PXE arch classes (option 93) - add the KeaAPI-spawned REST service: Terraform-friendly CRUD over subnet and client-class CRs (stable IDs, PUT upsert, 404 drift, bearer-token auth) - add Makefile (patch/minor/major tag targets), distroless operator/api images, an AlmaLinux+EPEL kea workload image, and woodpecker CI with k8s resources + serviceAccountName on every step - unit tests for config rendering, controller reconcile/config-hash, and the API Claude-Session: https://claude.ai/code/session_01JUoARVdmhxKQHyyyp1pxeT
118 lines
4.0 KiB
Go
118 lines
4.0 KiB
Go
package keaapi
|
|
|
|
import v1alpha1 "git.unkin.net/unkin/kea-operator/api/v1alpha1"
|
|
|
|
// OptionDataAPI is the wire form of a DHCP option value.
|
|
type OptionDataAPI struct {
|
|
Name string `json:"name,omitempty"`
|
|
Code int `json:"code,omitempty"`
|
|
Space string `json:"space,omitempty"`
|
|
Data string `json:"data"`
|
|
}
|
|
|
|
// SubnetAPI is the JSON contract for a subnet resource. Name is the stable id
|
|
// (the CR name) and is authoritative from the URL path.
|
|
type SubnetAPI struct {
|
|
Name string `json:"name"`
|
|
ClusterRef string `json:"cluster_ref,omitempty"`
|
|
Subnet string `json:"subnet"`
|
|
ID int `json:"id,omitempty"`
|
|
Pools []string `json:"pools,omitempty"`
|
|
Routers []string `json:"routers,omitempty"`
|
|
DNSServers []string `json:"dns_servers,omitempty"`
|
|
DomainName string `json:"domain_name,omitempty"`
|
|
NextServer string `json:"next_server,omitempty"`
|
|
BootFileName string `json:"boot_file_name,omitempty"`
|
|
ClientClasses []string `json:"client_classes,omitempty"`
|
|
ValidLifetime int `json:"valid_lifetime,omitempty"`
|
|
OptionData []OptionDataAPI `json:"option_data,omitempty"`
|
|
}
|
|
|
|
// ClientClassAPI is the JSON contract for a PXE client-class resource.
|
|
type ClientClassAPI struct {
|
|
Name string `json:"name"`
|
|
ClusterRef string `json:"cluster_ref,omitempty"`
|
|
Test string `json:"test,omitempty"`
|
|
ArchHex []string `json:"arch_hex,omitempty"`
|
|
BootFileName string `json:"boot_file_name,omitempty"`
|
|
NextServer string `json:"next_server,omitempty"`
|
|
ServerHostname string `json:"server_hostname,omitempty"`
|
|
OptionData []OptionDataAPI `json:"option_data,omitempty"`
|
|
}
|
|
|
|
func optionDataToAPI(in []v1alpha1.OptionData) []OptionDataAPI {
|
|
out := make([]OptionDataAPI, 0, len(in))
|
|
for _, o := range in {
|
|
out = append(out, OptionDataAPI{Name: o.Name, Code: o.Code, Space: o.Space, Data: o.Data})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func optionDataFromAPI(in []OptionDataAPI) []v1alpha1.OptionData {
|
|
out := make([]v1alpha1.OptionData, 0, len(in))
|
|
for _, o := range in {
|
|
out = append(out, v1alpha1.OptionData{Name: o.Name, Code: o.Code, Space: o.Space, Data: o.Data})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func subnetToAPI(s *v1alpha1.KeaSubnet) SubnetAPI {
|
|
return SubnetAPI{
|
|
Name: s.Name,
|
|
ClusterRef: s.Spec.ClusterRef,
|
|
Subnet: s.Spec.Subnet,
|
|
ID: s.Spec.ID,
|
|
Pools: s.Spec.Pools,
|
|
Routers: s.Spec.Routers,
|
|
DNSServers: s.Spec.DNSServers,
|
|
DomainName: s.Spec.DomainName,
|
|
NextServer: s.Spec.NextServer,
|
|
BootFileName: s.Spec.BootFileName,
|
|
ClientClasses: s.Spec.ClientClasses,
|
|
ValidLifetime: s.Spec.ValidLifetime,
|
|
OptionData: optionDataToAPI(s.Spec.OptionData),
|
|
}
|
|
}
|
|
|
|
func subnetSpecFromAPI(a SubnetAPI) v1alpha1.KeaSubnetSpec {
|
|
return v1alpha1.KeaSubnetSpec{
|
|
ClusterRef: a.ClusterRef,
|
|
Subnet: a.Subnet,
|
|
ID: a.ID,
|
|
Pools: a.Pools,
|
|
Routers: a.Routers,
|
|
DNSServers: a.DNSServers,
|
|
DomainName: a.DomainName,
|
|
NextServer: a.NextServer,
|
|
BootFileName: a.BootFileName,
|
|
ClientClasses: a.ClientClasses,
|
|
ValidLifetime: a.ValidLifetime,
|
|
OptionData: optionDataFromAPI(a.OptionData),
|
|
}
|
|
}
|
|
|
|
func classToAPI(c *v1alpha1.KeaClientClass) ClientClassAPI {
|
|
return ClientClassAPI{
|
|
Name: c.Name,
|
|
ClusterRef: c.Spec.ClusterRef,
|
|
Test: c.Spec.Test,
|
|
ArchHex: c.Spec.ArchHex,
|
|
BootFileName: c.Spec.BootFileName,
|
|
NextServer: c.Spec.NextServer,
|
|
ServerHostname: c.Spec.ServerHostname,
|
|
OptionData: optionDataToAPI(c.Spec.OptionData),
|
|
}
|
|
}
|
|
|
|
func classSpecFromAPI(a ClientClassAPI) v1alpha1.KeaClientClassSpec {
|
|
return v1alpha1.KeaClientClassSpec{
|
|
ClusterRef: a.ClusterRef,
|
|
Test: a.Test,
|
|
ArchHex: a.ArchHex,
|
|
BootFileName: a.BootFileName,
|
|
NextServer: a.NextServer,
|
|
ServerHostname: a.ServerHostname,
|
|
OptionData: optionDataFromAPI(a.OptionData),
|
|
}
|
|
}
|