From 9ee3ca15eb09fa6d46e2b69031c6041a8d6f5ec6 Mon Sep 17 00:00:00 2001 From: Pasha Sviderski Date: Fri, 25 Sep 2026 16:20:54 +1000 Subject: [PATCH] feat(dns): allow setting an externally managed cluster domain --- api/pb/cluster.pb.go | 140 ++++++++++-------- api/pb/cluster.proto | 6 +- api/pb/cluster_grpc.pb.go | 10 +- cmd/uc/caddy/deploy.go | 6 +- cmd/uc/dns/release.go | 1 + cmd/uc/dns/reserve.go | 3 +- cmd/uc/dns/root.go | 8 +- cmd/uc/dns/set.go | 38 +++-- cmd/uc/dns/show.go | 4 +- internal/machine/cluster/dns.go | 77 ++++++---- pkg/api/client.go | 2 +- pkg/api/dns.go | 8 + pkg/client/compose/deploy.go | 4 +- pkg/client/deploy/deploy.go | 4 +- pkg/client/dns.go | 27 +++- website/docs/9-cli-reference/uc.md | 2 +- website/docs/9-cli-reference/uc_dns.md | 8 +- .../docs/9-cli-reference/uc_dns_release.md | 2 +- .../docs/9-cli-reference/uc_dns_reserve.md | 2 +- website/docs/9-cli-reference/uc_dns_set.md | 15 +- website/docs/9-cli-reference/uc_dns_show.md | 2 +- 21 files changed, 233 insertions(+), 136 deletions(-) create mode 100644 pkg/api/dns.go diff --git a/api/pb/cluster.pb.go b/api/pb/cluster.pb.go index 677b3a81..8295a382 100644 --- a/api/pb/cluster.pb.go +++ b/api/pb/cluster.pb.go @@ -392,6 +392,9 @@ type Domain struct { unknownFields protoimpl.UnknownFields Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Whether the domain is reserved in Uncloud DNS. Older daemons (<0.21) omit this field + // and only support reserved domains, so clients should treat absence as true. + Reserved *bool `protobuf:"varint,2,opt,name=reserved,proto3,oneof" json:"reserved,omitempty"` } func (x *Domain) Reset() { @@ -433,6 +436,13 @@ func (x *Domain) GetName() string { return "" } +func (x *Domain) GetReserved() bool { + if x != nil && x.Reserved != nil { + return *x.Reserved + } + return false +} + type ReserveDomainRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -485,6 +495,7 @@ type SetDomainRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields + // An externally managed domain name. Empty clears a manually set domain. Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` } @@ -723,68 +734,72 @@ var file_api_pb_cluster_proto_rawDesc = []byte{ 0x65, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x52, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x26, 0x0a, 0x14, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x1c, 0x0a, 0x06, 0x44, 0x6f, 0x6d, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x4a, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x32, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x65, 0x72, - 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x1a, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x22, 0x26, 0x0a, 0x10, 0x53, - 0x65, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x22, 0x46, 0x0a, 0x1a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, - 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x28, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, - 0x72, 0x64, 0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x47, 0x0a, 0x1b, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, - 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x07, 0x72, 0x65, - 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, - 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x72, 0x65, 0x63, - 0x6f, 0x72, 0x64, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, - 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, - 0x63, 0x6f, 0x72, 0x64, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x52, - 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, - 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0x2e, 0x0a, - 0x0a, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0f, 0x0a, 0x0b, 0x55, - 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x05, 0x0a, 0x01, - 0x41, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x41, 0x41, 0x41, 0x10, 0x02, 0x32, 0xfb, 0x03, - 0x0a, 0x07, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x3d, 0x0a, 0x0a, 0x41, 0x64, 0x64, - 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, - 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, - 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x1a, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, - 0x6e, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, 0x0d, 0x52, - 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x19, 0x2e, 0x61, - 0x70, 0x69, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, - 0x37, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, - 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, - 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x61, 0x70, - 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x2f, 0x0a, 0x09, 0x53, 0x65, 0x74, 0x44, - 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x74, 0x44, - 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x61, - 0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x30, 0x0a, 0x09, 0x47, 0x65, 0x74, - 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0b, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x34, 0x0a, 0x0d, 0x52, - 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x16, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, - 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, - 0x6e, 0x12, 0x58, 0x0a, 0x13, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, - 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, - 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x70, 0x69, 0x2e, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, - 0x72, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, - 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x61, 0x70, 0x69, - 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x65, 0x72, + 0x76, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x08, 0x72, 0x65, 0x73, + 0x65, 0x72, 0x76, 0x65, 0x64, 0x88, 0x01, 0x01, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x72, 0x65, 0x73, + 0x65, 0x72, 0x76, 0x65, 0x64, 0x22, 0x32, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, + 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, + 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x22, 0x26, 0x0a, 0x10, 0x53, 0x65, 0x74, + 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x22, 0x46, 0x0a, 0x1a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, + 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x28, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, + 0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x22, 0x47, 0x0a, 0x1b, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f, + 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, + 0x64, 0x73, 0x22, 0x96, 0x01, 0x0a, 0x09, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, + 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x4e, 0x53, 0x52, 0x65, 0x63, 0x6f, + 0x72, 0x64, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0x2e, 0x0a, 0x0a, 0x52, + 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0f, 0x0a, 0x0b, 0x55, 0x4e, 0x53, + 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x05, 0x0a, 0x01, 0x41, 0x10, + 0x01, 0x12, 0x08, 0x0a, 0x04, 0x41, 0x41, 0x41, 0x41, 0x10, 0x02, 0x32, 0x86, 0x04, 0x0a, 0x07, + 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x3d, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x4d, 0x61, + 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d, + 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x41, 0x64, 0x64, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, + 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x19, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x42, 0x0a, 0x0d, 0x52, 0x65, 0x6d, + 0x6f, 0x76, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x19, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x37, 0x0a, + 0x0d, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x19, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x65, 0x44, 0x6f, 0x6d, 0x61, + 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x3a, 0x0a, 0x09, 0x53, 0x65, 0x74, 0x44, 0x6f, 0x6d, + 0x61, 0x69, 0x6e, 0x12, 0x15, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x74, 0x44, 0x6f, 0x6d, + 0x61, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x12, 0x30, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, + 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f, + 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x34, 0x0a, 0x0d, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x44, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0b, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x58, 0x0a, 0x13, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, + 0x73, 0x12, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, + 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, + 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -842,7 +857,7 @@ var file_api_pb_cluster_proto_depIdxs = []int32{ 5, // 18: api.Cluster.ListMachines:output_type -> api.ListMachinesResponse 16, // 19: api.Cluster.RemoveMachine:output_type -> google.protobuf.Empty 7, // 20: api.Cluster.ReserveDomain:output_type -> api.Domain - 7, // 21: api.Cluster.SetDomain:output_type -> api.Domain + 16, // 21: api.Cluster.SetDomain:output_type -> google.protobuf.Empty 7, // 22: api.Cluster.GetDomain:output_type -> api.Domain 7, // 23: api.Cluster.ReleaseDomain:output_type -> api.Domain 11, // 24: api.Cluster.CreateDomainRecords:output_type -> api.CreateDomainRecordsResponse @@ -994,6 +1009,7 @@ func file_api_pb_cluster_proto_init() { } } } + file_api_pb_cluster_proto_msgTypes[5].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/api/pb/cluster.proto b/api/pb/cluster.proto index c9269b0f..6956d578 100644 --- a/api/pb/cluster.proto +++ b/api/pb/cluster.proto @@ -14,7 +14,7 @@ service Cluster { rpc RemoveMachine(RemoveMachineRequest) returns (google.protobuf.Empty); rpc ReserveDomain(ReserveDomainRequest) returns (Domain); - rpc SetDomain(SetDomainRequest) returns (Domain); + rpc SetDomain(SetDomainRequest) returns (google.protobuf.Empty); rpc GetDomain(google.protobuf.Empty) returns (Domain); rpc ReleaseDomain(google.protobuf.Empty) returns (Domain); rpc CreateDomainRecords(CreateDomainRecordsRequest) returns (CreateDomainRecordsResponse); @@ -57,6 +57,9 @@ message RemoveMachineRequest { message Domain { string name = 1; + // Whether the domain is reserved in Uncloud DNS. Older daemons (<0.21) omit this field + // and only support reserved domains, so clients should treat absence as true. + optional bool reserved = 2; } message ReserveDomainRequest { @@ -64,6 +67,7 @@ message ReserveDomainRequest { } message SetDomainRequest { + // An externally managed domain name. Empty clears a manually set domain. string name = 1; } diff --git a/api/pb/cluster_grpc.pb.go b/api/pb/cluster_grpc.pb.go index 5ef40ded..b7b1bfa0 100644 --- a/api/pb/cluster_grpc.pb.go +++ b/api/pb/cluster_grpc.pb.go @@ -38,7 +38,7 @@ type ClusterClient interface { ListMachines(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListMachinesResponse, error) RemoveMachine(ctx context.Context, in *RemoveMachineRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) ReserveDomain(ctx context.Context, in *ReserveDomainRequest, opts ...grpc.CallOption) (*Domain, error) - SetDomain(ctx context.Context, in *SetDomainRequest, opts ...grpc.CallOption) (*Domain, error) + SetDomain(ctx context.Context, in *SetDomainRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) GetDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error) ReleaseDomain(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Domain, error) CreateDomainRecords(ctx context.Context, in *CreateDomainRecordsRequest, opts ...grpc.CallOption) (*CreateDomainRecordsResponse, error) @@ -92,9 +92,9 @@ func (c *clusterClient) ReserveDomain(ctx context.Context, in *ReserveDomainRequ return out, nil } -func (c *clusterClient) SetDomain(ctx context.Context, in *SetDomainRequest, opts ...grpc.CallOption) (*Domain, error) { +func (c *clusterClient) SetDomain(ctx context.Context, in *SetDomainRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(Domain) + out := new(emptypb.Empty) err := c.cc.Invoke(ctx, Cluster_SetDomain_FullMethodName, in, out, cOpts...) if err != nil { return nil, err @@ -140,7 +140,7 @@ type ClusterServer interface { ListMachines(context.Context, *emptypb.Empty) (*ListMachinesResponse, error) RemoveMachine(context.Context, *RemoveMachineRequest) (*emptypb.Empty, error) ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error) - SetDomain(context.Context, *SetDomainRequest) (*Domain, error) + SetDomain(context.Context, *SetDomainRequest) (*emptypb.Empty, error) GetDomain(context.Context, *emptypb.Empty) (*Domain, error) ReleaseDomain(context.Context, *emptypb.Empty) (*Domain, error) CreateDomainRecords(context.Context, *CreateDomainRecordsRequest) (*CreateDomainRecordsResponse, error) @@ -166,7 +166,7 @@ func (UnimplementedClusterServer) RemoveMachine(context.Context, *RemoveMachineR func (UnimplementedClusterServer) ReserveDomain(context.Context, *ReserveDomainRequest) (*Domain, error) { return nil, status.Errorf(codes.Unimplemented, "method ReserveDomain not implemented") } -func (UnimplementedClusterServer) SetDomain(context.Context, *SetDomainRequest) (*Domain, error) { +func (UnimplementedClusterServer) SetDomain(context.Context, *SetDomainRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method SetDomain not implemented") } func (UnimplementedClusterServer) GetDomain(context.Context, *emptypb.Empty) (*Domain, error) { diff --git a/cmd/uc/caddy/deploy.go b/cmd/uc/caddy/deploy.go index ebb3d2d3..02e5030a 100644 --- a/cmd/uc/caddy/deploy.go +++ b/cmd/uc/caddy/deploy.go @@ -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") diff --git a/cmd/uc/dns/release.go b/cmd/uc/dns/release.go index 6c8fb2da..8400cff9 100644 --- a/cmd/uc/dns/release.go +++ b/cmd/uc/dns/release.go @@ -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) diff --git a/cmd/uc/dns/reserve.go b/cmd/uc/dns/reserve.go index 7670a22f..794bb5e7 100644 --- a/cmd/uc/dns/reserve.go +++ b/cmd/uc/dns/reserve.go @@ -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 } diff --git a/cmd/uc/dns/root.go b/cmd/uc/dns/root.go index ab9c07e3..f7ef350f 100644 --- a/cmd/uc/dns/root.go +++ b/cmd/uc/dns/root.go @@ -7,11 +7,13 @@ 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(), diff --git a/cmd/uc/dns/set.go b/cmd/uc/dns/set.go index b737f7c2..5047b18f 100644 --- a/cmd/uc/dns/set.go +++ b/cmd/uc/dns/set.go @@ -8,6 +8,7 @@ import ( "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" @@ -17,9 +18,14 @@ func NewSetCommand() *cobra.Command { cmd := &cobra.Command{ Use: "set DOMAIN_NAME", Args: cobra.ExactArgs(1), - Short: "Set a cluster domain directly in the cluster.", - Long: "Set a cluster domain directly in the cluster, bypassing Uncloud DNS. " + - "This assumes the DNS is externally set up.", + 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]) @@ -30,10 +36,12 @@ func NewSetCommand() *cobra.Command { } func set(ctx context.Context, uncli *cli.CLI, name string) error { - labels, ok := dns.IsDomainName(name) - if !ok || labels < 3 { - return fmt.Errorf("domain '%s' is not a valid name", name) + 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 { @@ -41,17 +49,21 @@ func set(ctx context.Context, uncli *cli.CLI, name string) error { } defer clusterClient.Close() - domain, err := clusterClient.SetDomain(ctx, &pb.SetDomainRequest{Name: name}) + _, err = clusterClient.SetDomain(ctx, &pb.SetDomainRequest{Name: name}) 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 + return fmt.Errorf("set cluster domain: %w", err) } - fmt.Printf("Set cluster domain: %s\n", domain.Name) - - // No need to update anything as the domain records pointing to this cluster should be wildcard, so all - // names already exist. + 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 } diff --git a/cmd/uc/dns/show.go b/cmd/uc/dns/show.go index b9a1fc64..3f03073b 100644 --- a/cmd/uc/dns/show.go +++ b/cmd/uc/dns/show.go @@ -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 } diff --git a/internal/machine/cluster/dns.go b/internal/machine/cluster/dns.go index ffc9bb6c..2b16ef3a 100644 --- a/internal/machine/cluster/dns.go +++ b/internal/machine/cluster/dns.go @@ -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 } } diff --git a/pkg/api/client.go b/pkg/api/client.go index f8243f8a..8276dcf5 100644 --- a/pkg/api/client.go +++ b/pkg/api/client.go @@ -36,7 +36,7 @@ type ContainerClient interface { } type DNSClient interface { - GetDomain(ctx context.Context) (string, error) + GetDomain(ctx context.Context) (ClusterDomain, error) } type ImageClient interface { diff --git a/pkg/api/dns.go b/pkg/api/dns.go new file mode 100644 index 00000000..5f50f749 --- /dev/null +++ b/pkg/api/dns.go @@ -0,0 +1,8 @@ +package api + +// ClusterDomain is the domain name assigned to the cluster. +type ClusterDomain struct { + Name string + // Reserved reports whether the domain is reserved in Uncloud DNS or set and managed externally. + Reserved bool +} diff --git a/pkg/client/compose/deploy.go b/pkg/client/compose/deploy.go index 02bb6549..92d2b9ba 100644 --- a/pkg/client/compose/deploy.go +++ b/pkg/client/compose/deploy.go @@ -46,8 +46,8 @@ func NewDeploymentWithStrategy(ctx context.Context, cli Client, project *types.P return nil, fmt.Errorf("get cluster domain: %w", err) } resolver := &deploy.ServiceSpecResolver{ - // If the domain is not found (not reserved), an empty domain is used for the resolver. - ClusterDomain: domain, + // If no domain is configured, an empty domain is used for the resolver. + ClusterDomain: domain.Name, } return &Deployment{ diff --git a/pkg/client/deploy/deploy.go b/pkg/client/deploy/deploy.go index 73e3ea0d..4869c69e 100644 --- a/pkg/client/deploy/deploy.go +++ b/pkg/client/deploy/deploy.go @@ -306,8 +306,8 @@ func (d *Deployment) Plan(ctx context.Context) (ServicePlan, error) { return ServicePlan{}, fmt.Errorf("get cluster domain: %w", err) } specResolver := &ServiceSpecResolver{ - // If the domain is not found (not reserved), an empty domain is used for the resolver. - ClusterDomain: clusterDomain, + // If no domain is configured, an empty domain is used for the resolver. + ClusterDomain: clusterDomain.Name, } resolvedSpec, err := specResolver.Resolve(d.Spec) diff --git a/pkg/client/dns.go b/pkg/client/dns.go index 457a1a4f..0c13fe13 100644 --- a/pkg/client/dns.go +++ b/pkg/client/dns.go @@ -21,17 +21,21 @@ import ( "google.golang.org/grpc/status" ) -// GetDomain returns the cluster domain name or ErrNotFound if it hasn't been reserved yet. -func (cli *Client) GetDomain(ctx context.Context) (string, error) { +// GetDomain returns the cluster domain or api.ErrNotFound if none is configured. +func (cli *Client) GetDomain(ctx context.Context) (api.ClusterDomain, error) { domain, err := cli.ClusterClient.GetDomain(ctx, nil) if err != nil { - if status.Convert(err).Code() == codes.NotFound { - return "", api.ErrNotFound + if status.Code(err) == codes.NotFound { + return api.ClusterDomain{}, api.ErrNotFound } - return "", err + return api.ClusterDomain{}, err } - return domain.Name, nil + return api.ClusterDomain{ + Name: domain.Name, + // Older daemons (<0.21) omit Reserved and only support reserved domains. + Reserved: domain.Reserved == nil || domain.GetReserved(), + }, nil } var ErrNoReachableMachines = errors.New("no internet-reachable machines running service containers") @@ -39,8 +43,17 @@ var ErrNoReachableMachines = errors.New("no internet-reachable machines running // 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. +// in the resulting DNS configuration. The domain must be reserved in Uncloud DNS. +// Returns the created DNS records or an error. func (cli *Client) CreateIngressRecords(ctx context.Context, serviceID string) ([]*pb.DNSRecord, error) { + domain, err := cli.GetDomain(ctx) + if err != nil { + return nil, fmt.Errorf("get cluster domain: %w", err) + } + if !domain.Reserved { + return nil, status.Error(codes.FailedPrecondition, "cluster domain is not reserved in Uncloud DNS") + } + svc, err := cli.InspectService(ctx, serviceID) if err != nil { return nil, fmt.Errorf("inspect service '%s': %w", serviceID, err) diff --git a/website/docs/9-cli-reference/uc.md b/website/docs/9-cli-reference/uc.md index feb32eee..3d8dc948 100644 --- a/website/docs/9-cli-reference/uc.md +++ b/website/docs/9-cli-reference/uc.md @@ -18,7 +18,7 @@ A CLI tool for managing Uncloud resources such as machines, services, and volume * [uc caddy](uc_caddy.md) - Manage Caddy reverse proxy service. * [uc ctx](uc_ctx.md) - Switch between different cluster contexts. Contains subcommands to manage contexts. * [uc deploy](uc_deploy.md) - Deploy services from a Compose file. -* [uc dns](uc_dns.md) - Manage cluster domain in Uncloud DNS. +* [uc dns](uc_dns.md) - Manage the cluster domain. * [uc exec](uc_exec.md) - Execute a command in a running service container. * [uc image](uc_image.md) - Manage images on machines in the cluster. * [uc images](uc_images.md) - List images on machines in the cluster. diff --git a/website/docs/9-cli-reference/uc_dns.md b/website/docs/9-cli-reference/uc_dns.md index b4519622..186b9c5d 100644 --- a/website/docs/9-cli-reference/uc_dns.md +++ b/website/docs/9-cli-reference/uc_dns.md @@ -1,12 +1,14 @@ # uc dns -Manage cluster domain in Uncloud DNS. +Manage the cluster domain. ## Synopsis -Manage cluster domain in Uncloud DNS. +Manage the cluster domain. 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. +EXPERIMENTAL: Use 'uc dns set' to configure an externally managed domain instead. Uncloud does not manage DNS records for manually set domains. + ## Options ``` @@ -27,6 +29,6 @@ DNS commands allow you to reserve or release a unique 'xxxxxx.uncld.dev' domain * [uc](uc.md) - A CLI tool for managing Uncloud resources such as machines, services, and volumes. * [uc dns release](uc_dns_release.md) - Release the reserved cluster domain. * [uc dns reserve](uc_dns_reserve.md) - Reserve a cluster domain in Uncloud DNS. -* [uc dns set](uc_dns_set.md) - Set a cluster domain directly in the cluster. +* [uc dns set](uc_dns_set.md) - Set or unset an externally managed cluster domain (EXPERIMENTAL). * [uc dns show](uc_dns_show.md) - Print the cluster domain name. diff --git a/website/docs/9-cli-reference/uc_dns_release.md b/website/docs/9-cli-reference/uc_dns_release.md index 455d0af0..e0b83664 100644 --- a/website/docs/9-cli-reference/uc_dns_release.md +++ b/website/docs/9-cli-reference/uc_dns_release.md @@ -23,5 +23,5 @@ uc dns release [flags] ## See also -* [uc dns](uc_dns.md) - Manage cluster domain in Uncloud DNS. +* [uc dns](uc_dns.md) - Manage the cluster domain. diff --git a/website/docs/9-cli-reference/uc_dns_reserve.md b/website/docs/9-cli-reference/uc_dns_reserve.md index 0f09e3f9..1462f78e 100644 --- a/website/docs/9-cli-reference/uc_dns_reserve.md +++ b/website/docs/9-cli-reference/uc_dns_reserve.md @@ -24,5 +24,5 @@ uc dns reserve [flags] ## See also -* [uc dns](uc_dns.md) - Manage cluster domain in Uncloud DNS. +* [uc dns](uc_dns.md) - Manage the cluster domain. diff --git a/website/docs/9-cli-reference/uc_dns_set.md b/website/docs/9-cli-reference/uc_dns_set.md index 634ca461..f19323c8 100644 --- a/website/docs/9-cli-reference/uc_dns_set.md +++ b/website/docs/9-cli-reference/uc_dns_set.md @@ -1,15 +1,24 @@ # uc dns set -Set a cluster domain directly in the cluster. +Set or unset an externally managed cluster domain (EXPERIMENTAL). ## Synopsis -Set a cluster domain directly in the cluster, bypassing Uncloud DNS. This assumes the DNS is externally set up. +EXPERIMENTAL: Set the cluster domain used to generate ingress hostnames for services. +Configure wildcard DNS records for this domain with your DNS provider. Uncloud will not create, verify, update, or delete external DNS records. +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. ``` uc dns set DOMAIN_NAME [flags] ``` +## Examples + +``` + uc dns set apps.example.com + uc dns set "" +``` + ## Options ``` @@ -27,5 +36,5 @@ uc dns set DOMAIN_NAME [flags] ## See also -* [uc dns](uc_dns.md) - Manage cluster domain in Uncloud DNS. +* [uc dns](uc_dns.md) - Manage the cluster domain. diff --git a/website/docs/9-cli-reference/uc_dns_show.md b/website/docs/9-cli-reference/uc_dns_show.md index 9a7eaaf3..3e45553f 100644 --- a/website/docs/9-cli-reference/uc_dns_show.md +++ b/website/docs/9-cli-reference/uc_dns_show.md @@ -23,5 +23,5 @@ uc dns show [flags] ## See also -* [uc dns](uc_dns.md) - Manage cluster domain in Uncloud DNS. +* [uc dns](uc_dns.md) - Manage the cluster domain.