Files
kea-operator/internal/keaapi/models.go
T
unkinben d3fb5dcd1a
ci/woodpecker/pr/build Pipeline failed
ci/woodpecker/pr/pre-commit Pipeline failed
ci/woodpecker/pr/test Pipeline failed
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
2026-08-02 18:42:46 +10:00

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),
}
}