feat(dns): allow setting an externally managed cluster domain

This commit is contained in:
Pasha Sviderski committed 2026-09-25 16:20:54 +10:00
1 parent b28c458029
commit 9ee3ca15eb
21 files changed
+233 -136

No files matched your search

+51 -26
View File
@@ -4,20 +4,24 @@ import (
"context"
"encoding/json"
"errors"
"strings"
"github.com/miekg/dns"
"github.com/psviderski/uncloud/api/pb"
"github.com/psviderski/uncloud/internal/dns"
undns "github.com/psviderski/uncloud/internal/dns"
"github.com/psviderski/uncloud/internal/machine/store"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/emptypb"
)
// uncloudDNSKey is the key used to store the details of the reserved domain in the store.
// uncloudDNSKey stores the cluster domain and, when reserved, its Uncloud DNS credentials.
const uncloudDNSKey = "uncloud_dns"
type uncloudDNSDomain struct {
// Endpoint is the API endpoint of the Uncloud DNS service where the domain is reserved.
// An empty endpoint means the domain is managed externally.
Endpoint string
Name string
// TODO: encrypt the token in the store.
@@ -34,14 +38,14 @@ func (c *Cluster) ReserveDomain(ctx context.Context, req *pb.ReserveDomainReques
}
if _, err := c.storedDomain(ctx); err == nil {
return nil, status.Errorf(codes.AlreadyExists, "domain already reserved")
return nil, status.Error(codes.AlreadyExists, "cluster domain already configured")
} else {
if s := status.Convert(err); s.Code() != codes.NotFound {
return nil, err
}
}
dnsClient := dns.NewClient()
dnsClient := undns.NewClient()
name, token, err := dnsClient.ReserveDomain(req.Endpoint)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
@@ -60,7 +64,7 @@ func (c *Cluster) ReserveDomain(ctx context.Context, req *pb.ReserveDomainReques
return nil, status.Errorf(codes.Internal, "store reserved domain: %v", err)
}
return &pb.Domain{Name: name}, nil
return &pb.Domain{Name: name, Reserved: proto.Bool(true)}, nil
}
func (c *Cluster) GetDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Domain, error) {
@@ -73,7 +77,7 @@ func (c *Cluster) GetDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Domain,
return nil, err
}
return &pb.Domain{Name: domain.Name}, nil
return &pb.Domain{Name: domain.Name, Reserved: proto.Bool(domain.Endpoint != "")}, nil
}
func (c *Cluster) storedDomain(ctx context.Context) (uncloudDNSDomain, error) {
@@ -103,33 +107,54 @@ func (c *Cluster) ReleaseDomain(ctx context.Context, _ *emptypb.Empty) (*pb.Doma
if err != nil {
return nil, err
}
if domain.Endpoint == "" {
return nil, status.Error(codes.FailedPrecondition,
"cluster domain is set manually, use 'uc dns set \"\"' to unset it")
}
if err = c.store.Delete(ctx, uncloudDNSKey); err != nil {
return nil, status.Errorf(codes.Internal, "delete domain from store: %v", err)
}
// TODO: implement and call Uncloud DNS endpoint to release/delete the domain.
return &pb.Domain{Name: domain.Name}, nil
return &pb.Domain{Name: domain.Name, Reserved: proto.Bool(true)}, nil
}
func (c *Cluster) SetDomain(ctx context.Context, req *pb.SetDomainRequest) (*pb.Domain, error) {
func (c *Cluster) SetDomain(ctx context.Context, req *pb.SetDomainRequest) (*emptypb.Empty, error) {
if err := c.checkReady(); err != nil {
return nil, err
}
if _, err := c.storedDomain(ctx); err == nil {
return nil, status.Errorf(codes.AlreadyExists, "domain already reserved")
} else {
if s := status.Convert(err); s.Code() != codes.NotFound {
return nil, err
name := req.GetName()
if name != "" {
if labels, ok := dns.IsDomainName(name); !ok || labels < 2 {
return nil, status.Errorf(codes.InvalidArgument,
"invalid cluster domain '%s': must be a valid domain name with at least two labels", name)
}
}
name = strings.ToLower(strings.TrimSuffix(name, "."))
domain := uncloudDNSDomain{
Endpoint: "",
Name: req.Name,
Token: "",
existing, err := c.storedDomain(ctx)
if err != nil && status.Code(err) != codes.NotFound {
return nil, err
}
if name == "" {
if err == nil {
if existing.Endpoint != "" {
return nil, status.Error(codes.FailedPrecondition,
"cluster domain is reserved in Uncloud DNS, use 'uc dns release' to release it")
}
if err := c.store.Delete(ctx, uncloudDNSKey); err != nil {
return nil, status.Errorf(codes.Internal, "unset cluster domain: %v", err)
}
}
return &emptypb.Empty{}, nil
}
if err == nil {
return nil, status.Error(codes.AlreadyExists, "cluster domain already configured")
}
domain := uncloudDNSDomain{Name: name}
domainJSON, err := json.Marshal(domain)
if err != nil {
return nil, status.Errorf(codes.Internal, "marshal set domain for store: %v", err)
@@ -138,7 +163,7 @@ func (c *Cluster) SetDomain(ctx context.Context, req *pb.SetDomainRequest) (*pb.
return nil, status.Errorf(codes.Internal, "store set domain: %v", err)
}
return &pb.Domain{Name: req.Name}, nil
return &emptypb.Empty{}, nil
}
func (c *Cluster) CreateDomainRecords(
@@ -153,16 +178,16 @@ func (c *Cluster) CreateDomainRecords(
return nil, err
}
if domain.Endpoint == "" { // uc dns set has set a name
return &pb.CreateDomainRecordsResponse{}, nil
if domain.Endpoint == "" {
return nil, status.Error(codes.FailedPrecondition, "cluster domain is not reserved in Uncloud DNS")
}
dnsClient := dns.NewClient()
recordsReq := make([]dns.RecordRequest, len(req.Records))
dnsClient := undns.NewClient()
recordsReq := make([]undns.RecordRequest, len(req.Records))
for i, r := range req.Records {
recordsReq[i] = dns.RecordRequest{
recordsReq[i] = undns.RecordRequest{
Name: r.Name,
Type: dns.RecordType(r.Type.String()),
Type: undns.RecordType(r.Type.String()),
Values: r.Values,
}
}
@@ -182,9 +207,9 @@ func (c *Cluster) CreateDomainRecords(
}
switch r.Type {
case dns.RecordTypeA:
case undns.RecordTypeA:
resp.Records[i].Type = pb.DNSRecord_A
case dns.RecordTypeAAAA:
case undns.RecordTypeAAAA:
resp.Records[i].Type = pb.DNSRecord_AAAA
}
}