fix: add hostname to endpoints

Populate endpoint coming from the Kubernetes controlplane endpoint with
the hostname (if the endpoint is a hostname).

This should improve cases when hostname is used for the endpoint in
terms of SNI, proper resolving of DNS if it's dynamic.

See https://github.com/siderolabs/talos/pull/12556#issuecomment-3755862314

Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
(cherry picked from commit 96e604874b17e7aa8b62bfb25737f349e539bc5a)
This commit is contained in:
Andrey Smirnov 2026-01-15 22:32:55 +04:00 committed by Mateusz Urbanek
parent 624f9b52ab
commit b8f8245253
No known key found for this signature in database
GPG Key ID: F16F84591E26D77F
12 changed files with 184 additions and 20 deletions

View File

@ -93,9 +93,10 @@ message ControllerManagerConfigSpec {
Resources resources = 9;
}
// EndpointSpec describes status of rendered secrets.
// EndpointSpec describes a list of endpoints to connect to.
message EndpointSpec {
repeated common.NetIP addresses = 1;
repeated string hosts = 2;
}
// ExtraManifest defines a single extra manifest to download.

View File

@ -75,6 +75,7 @@ func (ctrl *StaticEndpointController) Run(ctx context.Context, r controller.Runt
var (
resolver net.Resolver
addrs []netip.Addr
hosts []string
)
addrs, err = resolver.LookupNetIP(ctx, "ip", cpHostname)
@ -84,8 +85,13 @@ func (ctrl *StaticEndpointController) Run(ctx context.Context, r controller.Runt
addrs = xslices.Map(addrs, netip.Addr.Unmap)
if len(addrs) != 1 || addrs[0].String() != cpHostname {
hosts = []string{cpHostname}
}
if err = safe.WriterModify(ctx, r, k8s.NewEndpoint(k8s.ControlPlaneNamespaceName, k8s.ControlPlaneKubernetesEndpointsID), func(endpoint *k8s.Endpoint) error {
endpoint.TypedSpec().Addresses = addrs
endpoint.TypedSpec().Hosts = hosts
return nil
}); err != nil {

View File

@ -51,6 +51,7 @@ func (suite *StaticEndpointControllerSuite) TestReconcile() {
rtestutils.AssertResources(suite.Ctx(), suite.T(), suite.State(), []resource.ID{k8s.ControlPlaneKubernetesEndpointsID},
func(endpoint *k8s.Endpoint, assert *assert.Assertions) {
assert.Equal([]netip.Addr{netip.MustParseAddr("2001:db8::1")}, endpoint.TypedSpec().Addresses)
assert.Empty(endpoint.TypedSpec().Hosts)
})
suite.Require().NoError(suite.State().Destroy(suite.Ctx(), cfg.Metadata()))
@ -58,6 +59,36 @@ func (suite *StaticEndpointControllerSuite) TestReconcile() {
rtestutils.AssertNoResource[*k8s.Endpoint](suite.Ctx(), suite.T(), suite.State(), k8s.ControlPlaneKubernetesEndpointsID)
}
func (suite *StaticEndpointControllerSuite) TestReconcileHostname() {
u, err := url.Parse("https://localhost:6443/")
suite.Require().NoError(err)
cfg := config.NewMachineConfig(
container.NewV1Alpha1(
&v1alpha1.Config{
ConfigVersion: "v1alpha1",
MachineConfig: &v1alpha1.MachineConfig{},
ClusterConfig: &v1alpha1.ClusterConfig{
ControlPlane: &v1alpha1.ControlPlaneConfig{
Endpoint: &v1alpha1.Endpoint{
URL: u,
},
},
},
},
),
)
suite.Require().NoError(suite.State().Create(suite.Ctx(), cfg))
rtestutils.AssertResources(suite.Ctx(), suite.T(), suite.State(), []resource.ID{k8s.ControlPlaneKubernetesEndpointsID},
func(endpoint *k8s.Endpoint, assert *assert.Assertions) {
// localhost might resolve to ::1 as well, check only for 127.0.0.1
assert.Contains(endpoint.TypedSpec().Addresses, netip.MustParseAddr("127.0.0.1"))
assert.Equal([]string{"localhost"}, endpoint.TypedSpec().Hosts)
})
}
func TestStaticEndpointControllerSuite(t *testing.T) {
t.Parallel()

View File

@ -108,7 +108,7 @@ func (ctrl *EndpointController) Run(ctx context.Context, r controller.Runtime, l
endpointAddrs = endpointAddrs.Merge(endpointResource)
}
if len(endpointAddrs) == 0 {
if endpointAddrs.IsEmpty() {
continue
}
@ -212,20 +212,20 @@ func (ctrl *EndpointController) ensureTalosEndpointSlices(ctx context.Context, l
addrsIPv6 k8s.EndpointList
)
for _, addr := range endpointAddrs {
for _, addr := range endpointAddrs.Addresses {
switch {
case addr.Is4():
addrsIPv4 = append(addrsIPv4, addr)
addrsIPv4.Addresses = append(addrsIPv4.Addresses, addr)
case addr.Is6():
addrsIPv6 = append(addrsIPv6, addr)
addrsIPv6.Addresses = append(addrsIPv6.Addresses, addr)
default:
// ignore other address types
}
}
if len(addrsIPv4) == 0 {
if len(addrsIPv4.Addresses) == 0 {
if err := ctrl.deleteTalosEndpointSlicesTyped(ctx, logger, client, discoveryv1.AddressTypeIPv4); err != nil {
return fmt.Errorf("error deleting Talos API endpoint slices for IPv4: %w", err)
}
@ -235,7 +235,7 @@ func (ctrl *EndpointController) ensureTalosEndpointSlices(ctx context.Context, l
}
}
if len(addrsIPv6) == 0 {
if len(addrsIPv6.Addresses) == 0 {
if err := ctrl.deleteTalosEndpointSlicesTyped(ctx, logger, client, discoveryv1.AddressTypeIPv6); err != nil {
return fmt.Errorf("error deleting Talos API endpoint slices for IPv6: %w", err)
}
@ -306,7 +306,7 @@ func (ctrl *EndpointController) ensureTalosEndpointSlicesTyped(
},
}
for _, addr := range endpointAddrs {
for _, addr := range endpointAddrs.Addresses {
newEndpointSlice.Endpoints = append(
newEndpointSlice.Endpoints,
discoveryv1.Endpoint{

View File

@ -257,7 +257,7 @@ func (ctrl *APIController) reconcile(ctx context.Context, r controller.Runtime,
endpointAddrs = endpointAddrs.Merge(res.(*k8s.Endpoint))
}
if len(endpointAddrs) == 0 {
if endpointAddrs.IsEmpty() {
continue
}

View File

@ -33,7 +33,7 @@ func GetEndpoints(ctx context.Context, resources state.State) ([]string, error)
endpointAddrs = endpointAddrs.Merge(res)
}
if len(endpointAddrs) == 0 {
if endpointAddrs.IsEmpty() {
return nil, errors.New("no controlplane endpoints discovered yet")
}

View File

@ -743,10 +743,11 @@ func (x *ControllerManagerConfigSpec) GetResources() *Resources {
return nil
}
// EndpointSpec describes status of rendered secrets.
// EndpointSpec describes a list of endpoints to connect to.
type EndpointSpec struct {
state protoimpl.MessageState `protogen:"open.v1"`
Addresses []*common.NetIP `protobuf:"bytes,1,rep,name=addresses,proto3" json:"addresses,omitempty"`
Hosts []string `protobuf:"bytes,2,rep,name=hosts,proto3" json:"hosts,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -788,6 +789,13 @@ func (x *EndpointSpec) GetAddresses() []*common.NetIP {
return nil
}
func (x *EndpointSpec) GetHosts() []string {
if x != nil {
return x.Hosts
}
return nil
}
// ExtraManifest defines a single extra manifest to download.
type ExtraManifest struct {
state protoimpl.MessageState `protogen:"open.v1"`
@ -2378,9 +2386,10 @@ const file_resource_definitions_k8s_k8s_proto_rawDesc = "" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1aG\n" +
"\x19EnvironmentVariablesEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\";\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"Q\n" +
"\fEndpointSpec\x12+\n" +
"\taddresses\x18\x01 \x03(\v2\r.common.NetIPR\taddresses\"\xa1\x02\n" +
"\taddresses\x18\x01 \x03(\v2\r.common.NetIPR\taddresses\x12\x14\n" +
"\x05hosts\x18\x02 \x03(\tR\x05hosts\"\xa1\x02\n" +
"\rExtraManifest\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" +
"\x03url\x18\x02 \x01(\tR\x03url\x12\x1a\n" +

View File

@ -810,6 +810,15 @@ func (m *EndpointSpec) MarshalToSizedBufferVT(dAtA []byte) (int, error) {
i -= len(m.unknownFields)
copy(dAtA[i:], m.unknownFields)
}
if len(m.Hosts) > 0 {
for iNdEx := len(m.Hosts) - 1; iNdEx >= 0; iNdEx-- {
i -= len(m.Hosts[iNdEx])
copy(dAtA[i:], m.Hosts[iNdEx])
i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Hosts[iNdEx])))
i--
dAtA[i] = 0x12
}
}
if len(m.Addresses) > 0 {
for iNdEx := len(m.Addresses) - 1; iNdEx >= 0; iNdEx-- {
if vtmsg, ok := interface{}(m.Addresses[iNdEx]).(interface {
@ -2747,6 +2756,12 @@ func (m *EndpointSpec) SizeVT() (n int) {
n += 1 + l + protohelpers.SizeOfVarint(uint64(l))
}
}
if len(m.Hosts) > 0 {
for _, s := range m.Hosts {
l = len(s)
n += 1 + l + protohelpers.SizeOfVarint(uint64(l))
}
}
n += len(m.unknownFields)
return n
}
@ -5731,6 +5746,38 @@ func (m *EndpointSpec) UnmarshalVT(dAtA []byte) error {
}
}
iNdEx = postIndex
case 2:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Hosts", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return protohelpers.ErrIntOverflow
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return protohelpers.ErrInvalidLength
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return protohelpers.ErrInvalidLength
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Hosts = append(m.Hosts, string(dAtA[iNdEx:postIndex]))
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := protohelpers.Skip(dAtA[iNdEx:])

View File

@ -175,6 +175,10 @@ func (o EndpointSpec) DeepCopy() EndpointSpec {
cp.Addresses = make([]netip.Addr, len(o.Addresses))
copy(cp.Addresses, o.Addresses)
}
if o.Hosts != nil {
cp.Hosts = make([]string, len(o.Hosts))
copy(cp.Hosts, o.Hosts)
}
return cp
}

View File

@ -32,11 +32,12 @@ const ControlPlaneKubernetesEndpointsID = resource.ID("controlplane")
// Endpoint resource holds definition of rendered secrets.
type Endpoint = typed.Resource[EndpointSpec, EndpointExtension]
// EndpointSpec describes status of rendered secrets.
// EndpointSpec describes a list of endpoints to connect to.
//
//gotagsrewrite:gen
type EndpointSpec struct {
Addresses []netip.Addr `yaml:"addresses" protobuf:"1"`
Hosts []string `yaml:"hosts" protobuf:"2"`
}
// NewEndpoint initializes the Endpoint resource.
@ -61,32 +62,56 @@ func (EndpointExtension) ResourceDefinition() meta.ResourceDefinitionSpec {
Name: "Addresses",
JSONPath: "{.addresses}",
},
{
Name: "Hosts",
JSONPath: "{.hosts}",
},
},
}
}
// EndpointList is a flattened list of endpoints.
type EndpointList []netip.Addr
type EndpointList struct {
Addresses []netip.Addr
Hosts []string
}
// Merge endpoints from multiple Endpoint resources into a single list.
func (l EndpointList) Merge(endpoint *Endpoint) EndpointList {
for _, ip := range endpoint.TypedSpec().Addresses {
idx, _ := slices.BinarySearchFunc(l, ip, func(a netip.Addr, target netip.Addr) int {
idx, _ := slices.BinarySearchFunc(l.Addresses, ip, func(a netip.Addr, target netip.Addr) int {
return a.Compare(target)
})
if idx < len(l) && l[idx].Compare(ip) == 0 {
if idx < len(l.Addresses) && l.Addresses[idx].Compare(ip) == 0 {
continue
}
l = slices.Insert(l, idx, ip)
l.Addresses = slices.Insert(l.Addresses, idx, ip)
}
for _, host := range endpoint.TypedSpec().Hosts {
idx, _ := slices.BinarySearch(l.Hosts, host)
if idx < len(l.Hosts) && l.Hosts[idx] == host {
continue
}
l.Hosts = slices.Insert(l.Hosts, idx, host)
}
return l
}
// IsEmpty checks if the EndpointList is empty.
func (l EndpointList) IsEmpty() bool {
return len(l.Addresses) == 0 && len(l.Hosts) == 0
}
// Strings returns a slice of formatted endpoints to string.
func (l EndpointList) Strings() []string {
return xslices.Map(l, netip.Addr.String)
return slices.Concat(
xslices.Map(l.Addresses, netip.Addr.String),
l.Hosts,
)
}
func init() {

View File

@ -35,3 +35,43 @@ func TestEndpointList(t *testing.T) {
assert.Equal(t, []string{"172.20.0.2", "172.20.0.3", "172.20.0.4"}, l.Strings())
}
func TestEndpointListWithHosts(t *testing.T) {
t.Parallel()
var l k8s.EndpointList
assert.True(t, l.IsEmpty())
e1 := k8s.NewEndpoint(k8s.ControlPlaneNamespaceName, "1")
e1.TypedSpec().Addresses = []netip.Addr{
netip.MustParseAddr("172.20.0.2"),
}
e1.TypedSpec().Hosts = []string{
"host1.example.com",
"host2.example.com",
}
e2 := k8s.NewEndpoint(k8s.ControlPlaneNamespaceName, "2")
e2.TypedSpec().Addresses = []netip.Addr{
netip.MustParseAddr("172.20.0.3"),
}
e2.TypedSpec().Hosts = []string{
"host2.example.com",
"host3.example.com",
}
l = l.Merge(e1)
l = l.Merge(e2)
assert.Equal(t,
[]string{
"172.20.0.2",
"172.20.0.3",
"host1.example.com",
"host2.example.com",
"host3.example.com",
},
l.Strings(),
)
}

View File

@ -7067,12 +7067,13 @@ ControllerManagerConfigSpec is configuration for kube-controller-manager.
<a name="talos.resource.definitions.k8s.EndpointSpec"></a>
### EndpointSpec
EndpointSpec describes status of rendered secrets.
EndpointSpec describes a list of endpoints to connect to.
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| addresses | [common.NetIP](#common.NetIP) | repeated | |
| hosts | [string](#string) | repeated | |