diff --git a/api/resource/definitions/k8s/k8s.proto b/api/resource/definitions/k8s/k8s.proto index 0232ea0a1..33083ddfb 100755 --- a/api/resource/definitions/k8s/k8s.proto +++ b/api/resource/definitions/k8s/k8s.proto @@ -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. diff --git a/internal/app/machined/pkg/controllers/k8s/static_endpoint.go b/internal/app/machined/pkg/controllers/k8s/static_endpoint.go index 9ec6076ed..ee93cf05c 100644 --- a/internal/app/machined/pkg/controllers/k8s/static_endpoint.go +++ b/internal/app/machined/pkg/controllers/k8s/static_endpoint.go @@ -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 { diff --git a/internal/app/machined/pkg/controllers/k8s/static_endpoint_test.go b/internal/app/machined/pkg/controllers/k8s/static_endpoint_test.go index d119e7f96..31b4dfc16 100644 --- a/internal/app/machined/pkg/controllers/k8s/static_endpoint_test.go +++ b/internal/app/machined/pkg/controllers/k8s/static_endpoint_test.go @@ -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() diff --git a/internal/app/machined/pkg/controllers/kubeaccess/endpoint.go b/internal/app/machined/pkg/controllers/kubeaccess/endpoint.go index bef3f00cc..b3d112c3d 100644 --- a/internal/app/machined/pkg/controllers/kubeaccess/endpoint.go +++ b/internal/app/machined/pkg/controllers/kubeaccess/endpoint.go @@ -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{ diff --git a/internal/app/machined/pkg/controllers/secrets/api.go b/internal/app/machined/pkg/controllers/secrets/api.go index 1bd711909..24ebb36b3 100644 --- a/internal/app/machined/pkg/controllers/secrets/api.go +++ b/internal/app/machined/pkg/controllers/secrets/api.go @@ -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 } diff --git a/internal/pkg/etcd/endpoints.go b/internal/pkg/etcd/endpoints.go index 5c91db7c3..5be4145cd 100644 --- a/internal/pkg/etcd/endpoints.go +++ b/internal/pkg/etcd/endpoints.go @@ -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") } diff --git a/pkg/machinery/api/resource/definitions/k8s/k8s.pb.go b/pkg/machinery/api/resource/definitions/k8s/k8s.pb.go index ed8f2e002..9e810acbe 100644 --- a/pkg/machinery/api/resource/definitions/k8s/k8s.pb.go +++ b/pkg/machinery/api/resource/definitions/k8s/k8s.pb.go @@ -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" + diff --git a/pkg/machinery/api/resource/definitions/k8s/k8s_vtproto.pb.go b/pkg/machinery/api/resource/definitions/k8s/k8s_vtproto.pb.go index f6eaa3cec..22193dca2 100644 --- a/pkg/machinery/api/resource/definitions/k8s/k8s_vtproto.pb.go +++ b/pkg/machinery/api/resource/definitions/k8s/k8s_vtproto.pb.go @@ -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:]) diff --git a/pkg/machinery/resources/k8s/deep_copy.generated.go b/pkg/machinery/resources/k8s/deep_copy.generated.go index 6d3783704..3b87ae930 100644 --- a/pkg/machinery/resources/k8s/deep_copy.generated.go +++ b/pkg/machinery/resources/k8s/deep_copy.generated.go @@ -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 } diff --git a/pkg/machinery/resources/k8s/endpoint.go b/pkg/machinery/resources/k8s/endpoint.go index 051e3ead6..5b897ab28 100644 --- a/pkg/machinery/resources/k8s/endpoint.go +++ b/pkg/machinery/resources/k8s/endpoint.go @@ -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() { diff --git a/pkg/machinery/resources/k8s/endpoint_test.go b/pkg/machinery/resources/k8s/endpoint_test.go index 930aebf77..94c2a0188 100644 --- a/pkg/machinery/resources/k8s/endpoint_test.go +++ b/pkg/machinery/resources/k8s/endpoint_test.go @@ -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(), + ) +} diff --git a/website/content/v1.12/reference/api.md b/website/content/v1.12/reference/api.md index 066ae4b05..d2befea71 100644 --- a/website/content/v1.12/reference/api.md +++ b/website/content/v1.12/reference/api.md @@ -7067,12 +7067,13 @@ ControllerManagerConfigSpec is configuration for kube-controller-manager. ### 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 | |