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

* feat: add uc dns set

This allows manual setting of the cluster's domain name. I use this to
make the local cluster admin aware that this has been done.
(and then manually update the external dns)

```
% uc dns set ctrl.u.science.ru.nl
Set cluster domain: ctrl.u.science.ru.nl
Deploy the Caddy reverse proxy service ('uc caddy deploy') to enable internet access to your services via the reserved or your custom domain.

% uc dns show
ctrl.u.science.ru.nl

% uc dns set ctrl.u.science.ru.nl
Error: domain already reserved

% uc dns release
Released cluster domain: ctrl.u.science.ru.nl
```

See: #37

Signed-off-by: Miek Gieben <miek@miek.nl>

* docs

Signed-off-by: Miek Gieben <miek@miek.nl>

* check validatity

Signed-off-by: Miek Gieben <miek@miek.nl>

* No need to call out to update anything, as set doesnt use uncloud dns

Signed-off-by: Miek Gieben <miek@miek.nl>

* Ignore domain when there is no endpoint

Prevents this error, when you know what you're doing:

    Error: failed to update DNS records pointing to caddy service: create cluster domain records in Uncloud DNS: rpc error: code = Unknown desc = Post "/domains/ctrl.u.science.ru.nl/records": unsupported protocol scheme ""

Signed-off-by: Miek Gieben <miek@miek.nl>

* from main

Signed-off-by: Miek Gieben <miek@miek.nl>

* mise proto && make cli-docs

Signed-off-by: Miek Gieben <miek@miek.nl>

* Rebase and and generate protos again

Signed-off-by: Miek Gieben <miek@miek.nl>

* Rebase and fix

Signed-off-by: Miek Gieben <miek@miek.nl>

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

---------

Signed-off-by: Miek Gieben <miek@miek.nl>
Co-authored-by: Pasha Sviderski <me@psviderski.name>
This commit is contained in:
Miek GiebenandPasha Sviderski authored and GitHub committed 2026-09-25 16:36:01 +10:00
1 parent 3482eeefab
commit 417e402c73
21 files changed
+463 -133

No files matched your search

+5 -1
View File
@@ -202,6 +202,10 @@ func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, prog
}
return fmt.Errorf("get cluster domain: %w", err)
}
if !domain.Reserved {
fmt.Printf("Skipping DNS records update as cluster domain '%s' is managed externally.\n", domain.Name)
return nil
}
fmt.Println("Updating cluster domain records in Uncloud DNS to point to machines running caddy service...")
// TODO: split the method into two: one to get the records and one to update them to ask for update confirmation.
@@ -216,7 +220,7 @@ func UpdateDomainRecords(ctx context.Context, clusterClient *client.Client, prog
if errors.Is(err, client.ErrNoReachableMachines) {
fmt.Println()
fmt.Printf("DNS records for domain '%s' could not be updated as there are no internet-reachable "+
"machines running caddy containers.\n", domain)
"machines running caddy containers.\n", domain.Name)
fmt.Println()
fmt.Println("Possible solutions:")
fmt.Println("- Ensure your machines have public IP addresses")
+1
View File
@@ -37,6 +37,7 @@ func release(ctx context.Context, uncli *cli.CLI) error {
if status.Convert(err).Code() == codes.NotFound {
return errors.New("no domain reserved")
}
return fmt.Errorf("release cluster domain: %w", err)
}
fmt.Printf("Released cluster domain: %s\n", domain.Name)
+2 -1
View File
@@ -49,7 +49,8 @@ func reserve(ctx context.Context, uncli *cli.CLI, opts reserveOptions) error {
domain, err := clusterClient.ReserveDomain(ctx, &pb.ReserveDomainRequest{Endpoint: opts.endpoint})
if err != nil {
if status.Convert(err).Code() == codes.AlreadyExists {
return errors.New("domain already reserved")
return errors.New("cluster domain already configured, unset it with 'uc dns set \"\"' " +
"or release a reservation with 'uc dns release' first")
}
return err
}
+6 -3
View File
@@ -7,16 +7,19 @@ import (
func NewRootCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "dns",
Short: "Manage cluster domain in Uncloud DNS.",
Long: "Manage cluster domain in Uncloud DNS.\n" +
Short: "Manage the cluster domain.",
Long: "Manage the cluster domain.\n" +
"DNS commands allow you to reserve or release a unique 'xxxxxx.uncld.dev' domain for your " +
"cluster. When reserved, Caddy service deployments will automatically update DNS records to route " +
"traffic to the services in the cluster.",
"traffic to the services in the cluster.\n\n" +
"EXPERIMENTAL: Use 'uc dns set' to configure an externally managed domain instead. " +
"Uncloud does not manage DNS records for manually set domains.",
}
cmd.AddCommand(
NewReleaseCommand(),
NewReserveCommand(),
NewShowCommand(),
NewSetCommand(),
)
return cmd
}
+69
View File
@@ -0,0 +1,69 @@
package dns
import (
"context"
"errors"
"fmt"
"github.com/miekg/dns"
"github.com/psviderski/uncloud/api/pb"
"github.com/psviderski/uncloud/internal/cli"
"github.com/psviderski/uncloud/internal/cli/tui"
"github.com/spf13/cobra"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func NewSetCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "set DOMAIN_NAME",
Args: cobra.ExactArgs(1),
Short: "Set or unset an externally managed cluster domain (EXPERIMENTAL).",
Long: "EXPERIMENTAL: Set the cluster domain used to generate ingress hostnames for services.\n" +
"Configure wildcard DNS records for this domain with your DNS provider. " +
"Uncloud will not create, verify, update, or delete external DNS records.\n" +
"Pass an empty string to unset a manually set domain. " +
"Use 'uc dns release' to release a domain reserved in Uncloud DNS. " +
"Setting or unsetting the domain does not change existing service hostnames.",
Example: " uc dns set apps.example.com\n uc dns set \"\"",
RunE: func(cmd *cobra.Command, args []string) error {
uncli := cmd.Context().Value("cli").(*cli.CLI)
return set(cmd.Context(), uncli, args[0])
},
}
return cmd
}
func set(ctx context.Context, uncli *cli.CLI, name string) error {
if name != "" {
if labels, ok := dns.IsDomainName(name); !ok || labels < 2 {
return fmt.Errorf("invalid cluster domain %q: must be a valid domain name with at least two labels", name)
}
}
tui.PrintWarning("Setting an externally managed cluster domain is experimental.")
clusterClient, err := uncli.ConnectCluster(ctx)
if err != nil {
return fmt.Errorf("connect to cluster: %w", err)
}
defer clusterClient.Close()
_, err = clusterClient.SetDomain(ctx, &pb.SetDomainRequest{Name: name})
if err != nil {
if status.Convert(err).Code() == codes.AlreadyExists {
return errors.New("cluster domain already configured, unset it with 'uc dns set \"\"' or " +
"release a reservation with 'uc dns release' first")
}
return fmt.Errorf("set cluster domain: %w", err)
}
if name == "" {
fmt.Println("Unset cluster domain. External DNS records have not been changed.")
return nil
}
fmt.Printf("Set cluster domain: %s\n", name)
fmt.Println("DNS is managed externally. " +
"Configure wildcard DNS records pointing to machines running the Caddy service.")
return nil
}
+2 -2
View File
@@ -33,11 +33,11 @@ func show(ctx context.Context, uncli *cli.CLI) error {
domain, err := clusterClient.GetDomain(ctx)
if err != nil {
if errors.Is(err, api.ErrNotFound) {
return errors.New("no domain reserved")
return errors.New("no cluster domain configured")
}
return err
}
fmt.Println(domain)
fmt.Println(domain.Name)
return nil
}