Scaffold kea-operator: CRDs, controllers, config rendering, REST API, CI
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
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
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),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user