feat(dns): update ingress records in Uncloud DNS when deploying Caddy

This commit is contained in:
Pavel Sviderski
2025-02-26 14:23:00 +10:00
parent 5e9b883ad5
commit 09bea1d12d
3 changed files with 152 additions and 16 deletions
+122 -9
View File
@@ -2,24 +2,128 @@ package client
import (
"context"
"errors"
"fmt"
"github.com/docker/compose/v2/pkg/progress"
"io"
"net/http"
"sync"
"time"
"uncloud/internal/machine/api/pb"
"uncloud/internal/machine/caddyfile"
)
// TODO:
var ErrNoReachableMachines = errors.New("no internet-reachable machines running service containers")
// CreateIngressRecords verifies which machines running the specified service (typically Caddy) are reachable from
// the internet, then creates DNS records for the cluster domain pointing to those machines. It tests each machine
// by sending HTTP requests to their public IPs. Only machines that respond correctly with their machine ID are included
// in the resulting DNS configuration. Returns the created DNS records or an error.
func (cli *Client) CreateIngressRecords(ctx context.Context, serviceID string) ([]*pb.DNSRecord, error) {
// TODO:
// - Inspect the service and get the list of machines it runs on.
// - For each machine get the machine's public IP address(s).
// - Update the wildcard DNS record for the service with the public IP addresses (call Cluster API).
svc, err := cli.InspectService(ctx, serviceID)
if err != nil {
return nil, fmt.Errorf("inspect service '%s': %w", serviceID, err)
}
machineIDs := make(map[string]struct{}, len(svc.Containers))
for _, mc := range svc.Containers {
machineIDs[mc.MachineID] = struct{}{}
}
var wg sync.WaitGroup
reachableMachines := make(chan *pb.MachineInfo)
for id := range machineIDs {
m, err := cli.InspectMachine(ctx, id)
if err != nil {
return nil, fmt.Errorf("inspect machine '%s': %w", id, err)
}
if m.Machine.PublicIp == nil {
continue
}
wg.Add(1)
go func() {
defer wg.Done()
// Verify that the Caddy container is reachable on the machine by its public IP.
publicIP, _ := m.Machine.PublicIp.ToAddr()
pw := progress.ContextWriter(ctx)
eventID := fmt.Sprintf("Machine %s (%s)", m.Machine.Name, publicIP)
pw.Event(progress.NewEvent(eventID, progress.Working, "Querying"))
verifyURL := fmt.Sprintf("http://%s%s", publicIP, caddyfile.VerifyPath)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, verifyURL, nil)
if err != nil {
pw.Event(progress.NewEvent(eventID, progress.Error, err.Error()))
return
}
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
e := unreachable(eventID)
e.Text = fmt.Sprintf("Failed to send HTTP request: %v", err)
pw.Event(e)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
e := unreachable(eventID)
e.Text = fmt.Sprintf("Unexpected HTTP response status code: %d", resp.StatusCode)
pw.Event(e)
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
e := unreachable(eventID)
e.Text = fmt.Sprintf("Failed to read HTTP response body: %v", err)
pw.Event(e)
return
}
// Check the response body is the machine ID to ensure the correct Caddy container is responding.
if string(body) == m.Machine.Id {
pw.Event(progress.NewEvent(eventID, progress.Done, "Reachable"))
reachableMachines <- m.Machine
} else {
bodyStr := string(body)
if len(bodyStr) > 50 {
bodyStr = bodyStr[:50] + "..."
}
e := unreachable(eventID)
e.Text = fmt.Sprintf("Unexpected HTTP response body: %s", bodyStr)
pw.Event(e)
}
}()
}
go func() {
wg.Wait()
close(reachableMachines)
}()
var ingressIPs []string
for m := range reachableMachines {
ip, _ := m.PublicIp.ToAddr()
ingressIPs = append(ingressIPs, ip.String())
}
if len(ingressIPs) == 0 {
return nil, ErrNoReachableMachines
}
req := &pb.CreateDomainRecordsRequest{
Records: []*pb.DNSRecord{
{
Name: "*",
Type: pb.DNSRecord_A,
// TODO: Get the public IP addresses of the machines running Caddy containers.
Values: []string{"1.2.3.4", "5.6.7.8"},
Name: "*",
Type: pb.DNSRecord_A,
Values: ingressIPs,
},
// TODO: Add AAAA record with routable IPv6 addresses of machines running Caddy containers.
},
@@ -31,3 +135,12 @@ func (cli *Client) CreateIngressRecords(ctx context.Context, serviceID string) (
return resp.Records, nil
}
// unreachable creates a new Unreachable error event.
func unreachable(id string) progress.Event {
return progress.NewEvent(
id,
progress.Error,
"Unreachable (probably behind NAT or firewall)",
)
}