- PATCH /agents/{id} switches the mode and reconnects the agent
- POST /revisions/{n}/deploy creates pending push deployments, sends
ApplyRevisionCommand with the deployment ID, re-sends open pushes on
connect and announces them in GetDesiredState; reports with the ID
complete the deployment
- POST /revisions/{n}/rollback re-renders the stored generator input
with the next revision number
- GET /revisions/{n}/diff and GET /config/pending with unified diffs
- GET /events streams agent, deployment, revision and pending events
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
156 lines
4.8 KiB
Go
156 lines
4.8 KiB
Go
// SPDX-License-Identifier: LicenseRef-DDI-Proprietary
|
|
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"git.beugsiud.de/beug.k/DDI/server/internal/configgen"
|
|
"git.beugsiud.de/beug.k/DDI/server/internal/configgen/bindgen"
|
|
"git.beugsiud.de/beug.k/DDI/server/internal/configgen/derive"
|
|
"git.beugsiud.de/beug.k/DDI/server/internal/configgen/keagen"
|
|
"git.beugsiud.de/beug.k/DDI/server/internal/ipam"
|
|
)
|
|
|
|
// previewSerial is the SOA serial shown in the preview; revisions use their
|
|
// own number.
|
|
const previewSerial = 1
|
|
|
|
// GeneratedFile is one generated file.
|
|
type GeneratedFile struct {
|
|
Path string `json:"path"`
|
|
Content string `json:"content"`
|
|
CreateOnly bool `json:"create_only"`
|
|
}
|
|
|
|
// GeneratorProblem explains why a part could not be generated.
|
|
type GeneratorProblem struct {
|
|
Code string `json:"code"`
|
|
Params map[string]string `json:"params,omitempty"`
|
|
}
|
|
|
|
// ConfigPreview is the generated configuration for the current data.
|
|
type ConfigPreview struct {
|
|
DNS []GeneratedFile `json:"dns"`
|
|
DHCP []GeneratedFile `json:"dhcp"`
|
|
Warnings []configgen.Warning `json:"warnings"`
|
|
DNSError *GeneratorProblem `json:"dns_error,omitempty"`
|
|
DHCPError *GeneratorProblem `json:"dhcp_error,omitempty"`
|
|
}
|
|
|
|
// loadSource reads everything the generator needs. serial becomes the SOA
|
|
// serial of every zone.
|
|
func (s *Service) loadSource(ctx context.Context, serial uint32) (configgen.Source, error) {
|
|
tx := s.st.Q()
|
|
nets, err := tx.ListNetworks(ctx)
|
|
if err != nil {
|
|
return configgen.Source{}, translate(err)
|
|
}
|
|
addrs, err := tx.AllAddresses(ctx)
|
|
if err != nil {
|
|
return configgen.Source{}, translate(err)
|
|
}
|
|
ids, err := tx.SubnetIDs(ctx)
|
|
if err != nil {
|
|
return configgen.Source{}, translate(err)
|
|
}
|
|
settings, err := s.GetGeneratorSettings(ctx)
|
|
if err != nil {
|
|
return configgen.Source{}, err
|
|
}
|
|
src := configgen.Source{
|
|
Serial: serial,
|
|
Addresses: addrs,
|
|
SubnetIDs: ids,
|
|
DNS: settings.dnsSettings(),
|
|
DHCP: configgen.DHCPSettings{
|
|
Interfaces: settings.DHCP.Interfaces, DirectClients: settings.DHCP.DirectClients,
|
|
ControlSocket: settings.DHCP.ControlSocket, LeaseFile: settings.DHCP.LeaseFile,
|
|
HooksDir: settings.DHCP.HooksDir, ValidLifetime: settings.DHCP.ValidLifetime,
|
|
},
|
|
}
|
|
src.Networks = make([]ipam.Network, len(nets))
|
|
for i, n := range nets {
|
|
src.Networks[i] = *n
|
|
}
|
|
return src, nil
|
|
}
|
|
|
|
func files(arts []configgen.Artifact) []GeneratedFile {
|
|
out := make([]GeneratedFile, len(arts)) // never nil: the API returns []
|
|
for i, a := range arts {
|
|
out[i] = GeneratedFile{Path: a.Path, Content: string(a.Content), CreateOnly: a.CreateOnly}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func generatorProblem(err error) *GeneratorProblem {
|
|
var ve *configgen.ValidationError
|
|
if errors.As(err, &ve) {
|
|
return &GeneratorProblem{Code: "config_invalid", Params: map[string]string{"path": ve.Path, "message": ve.Message}}
|
|
}
|
|
return &GeneratorProblem{Code: "config_failed", Params: map[string]string{"message": err.Error()}}
|
|
}
|
|
|
|
// PreviewConfig generates the BIND and Kea configuration for the current
|
|
// data without storing or deploying anything.
|
|
func (s *Service) PreviewConfig(ctx context.Context) (*ConfigPreview, error) {
|
|
g, err := s.generate(ctx, previewSerial)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := &ConfigPreview{
|
|
DNS: files(g.DNS), DHCP: files(g.DHCP), Warnings: g.Warnings,
|
|
DNSError: g.DNSError, DHCPError: g.DHCPError,
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// Generated is the output of one generator run. A part that could not be
|
|
// generated has no artifacts and a problem instead.
|
|
type Generated struct {
|
|
DNS []configgen.Artifact
|
|
DHCP []configgen.Artifact
|
|
Warnings []configgen.Warning
|
|
DNSError *GeneratorProblem
|
|
DHCPError *GeneratorProblem
|
|
}
|
|
|
|
// generate runs the generator on the current data (read outside a
|
|
// transaction; callers that need a consistent snapshot hold their own).
|
|
func (s *Service) generate(ctx context.Context, serial uint32) (*Generated, error) {
|
|
src, err := s.loadSource(ctx, serial)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return render(src), nil
|
|
}
|
|
|
|
// render runs the generator on src (a pure function: equal input gives
|
|
// byte-identical output).
|
|
func render(src configgen.Source) *Generated {
|
|
out := &Generated{Warnings: []configgen.Warning{}}
|
|
|
|
if src.DNS.SOA.PrimaryNS == "" || src.DNS.SOA.Hostmaster == "" || len(src.DNS.NameServers) == 0 {
|
|
out.DNSError = &GeneratorProblem{Code: "dns_settings_missing"}
|
|
} else {
|
|
dnsCfg, warnings := derive.BuildDNS(src)
|
|
out.Warnings = append(out.Warnings, warnings...)
|
|
if arts, err := bindgen.Render(dnsCfg); err != nil {
|
|
out.DNSError = generatorProblem(err)
|
|
} else {
|
|
out.DNS = arts
|
|
}
|
|
}
|
|
|
|
dhcpCfg, warnings := derive.BuildDHCP(src)
|
|
out.Warnings = append(out.Warnings, warnings...)
|
|
if arts, err := keagen.Render(dhcpCfg); err != nil {
|
|
out.DHCPError = generatorProblem(err)
|
|
} else {
|
|
out.DHCP = arts
|
|
}
|
|
return out
|
|
}
|