mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-28 20:13:33 +00:00
feat(dns): update ingress records in Uncloud DNS when deploying Caddy
This commit is contained in:
@@ -183,17 +183,38 @@ func deploy(ctx context.Context, uncli *cli.CLI, opts deployOptions) error {
|
|||||||
return fmt.Errorf("get cluster domain: %w", err)
|
return fmt.Errorf("get cluster domain: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Updating cluster domain records in Uncloud DNS to point to machines running caddy containers...")
|
fmt.Println("Updating cluster domain records in Uncloud DNS to point to machines running caddy service...")
|
||||||
records, err := clusterClient.CreateIngressRecords(ctx, client.CaddyServiceName)
|
// TODO: split the method into two: one to get the records and one to update them to ask for update confirmation.
|
||||||
|
|
||||||
|
var records []*pb.DNSRecord
|
||||||
|
err = progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
|
records, err = clusterClient.CreateIngressRecords(ctx, client.CaddyServiceName)
|
||||||
|
return err
|
||||||
|
}, uncli.ProgressOut(), "Verifying internet access to caddy service")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("update ingress records: %w", err)
|
if errors.Is(err, client.ErrNoReachableMachines) {
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("DNS records could not be updated as there are no internet-reachable machines running " +
|
||||||
|
"caddy containers.")
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Possible solutions:")
|
||||||
|
fmt.Println("- Ensure your machines have public IP addresses")
|
||||||
|
fmt.Println("- Use --public-ip flag when adding machines to override the automatically detected IPs")
|
||||||
|
fmt.Println("- Check firewall settings on your machines")
|
||||||
|
fmt.Println("- Configure port forwarding if behind NAT")
|
||||||
|
fmt.Println("- Retry Caddy deployment after resolving connectivity issues with 'uc caddy deploy'")
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Your services will not be accessible from the internet until at least one machine " +
|
||||||
|
"becomes reachable.")
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to update DNS records pointing to caddy service: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("DNS records updated successfully:")
|
|
||||||
for _, r := range records {
|
|
||||||
fmt.Printf(" %s %s -> %s", r.Name, r.Type, strings.Join(r.Values, ", "))
|
|
||||||
}
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
fmt.Println("DNS records updated to use only the internet-reachable machines running caddy service:")
|
||||||
|
for _, r := range records {
|
||||||
|
fmt.Printf(" %s %s -> %s\n", r.Name, r.Type, strings.Join(r.Values, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+122
-9
@@ -2,24 +2,128 @@ package client
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
"uncloud/internal/machine/api/pb"
|
"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) {
|
func (cli *Client) CreateIngressRecords(ctx context.Context, serviceID string) ([]*pb.DNSRecord, error) {
|
||||||
// TODO:
|
svc, err := cli.InspectService(ctx, serviceID)
|
||||||
// - Inspect the service and get the list of machines it runs on.
|
if err != nil {
|
||||||
// - For each machine get the machine's public IP address(s).
|
return nil, fmt.Errorf("inspect service '%s': %w", serviceID, err)
|
||||||
// - Update the wildcard DNS record for the service with the public IP addresses (call Cluster API).
|
}
|
||||||
|
|
||||||
|
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{
|
req := &pb.CreateDomainRecordsRequest{
|
||||||
Records: []*pb.DNSRecord{
|
Records: []*pb.DNSRecord{
|
||||||
{
|
{
|
||||||
Name: "*",
|
Name: "*",
|
||||||
Type: pb.DNSRecord_A,
|
Type: pb.DNSRecord_A,
|
||||||
// TODO: Get the public IP addresses of the machines running Caddy containers.
|
Values: ingressIPs,
|
||||||
Values: []string{"1.2.3.4", "5.6.7.8"},
|
|
||||||
},
|
},
|
||||||
// TODO: Add AAAA record with routable IPv6 addresses of machines running Caddy containers.
|
// 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
|
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)",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -403,6 +403,8 @@ func TestDeployment(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, c.Machines[2].Name, machine2.Machine.Name)
|
assert.Equal(t, c.Machines[2].Name, machine2.Machine.Name)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO: test deployments with unreachable machines. See https://github.com/psviderski/uncloud/issues/29.
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunService(t *testing.T) {
|
func TestRunService(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user