diff --git a/cmd/talosctl/cmd/mgmt/cluster/create/clusterops/configmaker/internal/makers/common_test.go b/cmd/talosctl/cmd/mgmt/cluster/create/clusterops/configmaker/internal/makers/common_test.go index 30f5fadb0..39d298fb7 100644 --- a/cmd/talosctl/cmd/mgmt/cluster/create/clusterops/configmaker/internal/makers/common_test.go +++ b/cmd/talosctl/cmd/mgmt/cluster/create/clusterops/configmaker/internal/makers/common_test.go @@ -19,6 +19,7 @@ import ( "github.com/siderolabs/talos/pkg/machinery/config/encoder" "github.com/siderolabs/talos/pkg/machinery/config/generate" "github.com/siderolabs/talos/pkg/machinery/config/generate/secrets" + "github.com/siderolabs/talos/pkg/machinery/version" "github.com/siderolabs/talos/pkg/provision" ) @@ -150,7 +151,8 @@ func TestCommonMaker_MachineConfig(t *testing.T) { // assertConfigDefaultness makes sure the maker-generated machine configs are not different from default talos machine configs. func assertConfigDefaultness[ExtraOps any](t *testing.T, cOps clusterops.Common, m makers.Maker[ExtraOps], desiredExtraGenOps []generate.Option, extraPatches ...configpatcher.Patch) { - var versionContract *config.VersionContract + versionContract, err := config.ParseContractFromVersion(version.Tag) + require.NoError(t, err) secretsBundle, err := secrets.NewBundle(secrets.NewClock(), versionContract) require.NoError(t, err) diff --git a/hack/release.toml b/hack/release.toml index 4669d97e5..6148bedfe 100644 --- a/hack/release.toml +++ b/hack/release.toml @@ -45,6 +45,12 @@ Custom settings for cipher suites have been removed, as they are ignored when TL title = "Default Installer Image" description = """\ The default installer image has been updated to use the Image Factory. +""" + + [notes.hostdnsconfig] + title = "Host DNS Configuration" + description = """\ +HostDNS configuration was moved from the v1alpha1 config `.machine.features.hostDNS` field to the new `hostDNS` in the `ResolverConfig` document. """ [make_deps] diff --git a/internal/app/machined/pkg/controllers/network/hostdns_config.go b/internal/app/machined/pkg/controllers/network/hostdns_config.go index 82d5739ae..7bb56af8d 100644 --- a/internal/app/machined/pkg/controllers/network/hostdns_config.go +++ b/internal/app/machined/pkg/controllers/network/hostdns_config.go @@ -17,7 +17,7 @@ import ( "github.com/siderolabs/go-procfs/procfs" "go.uber.org/zap" - talosconfig "github.com/siderolabs/talos/pkg/machinery/config" + cfgcfg "github.com/siderolabs/talos/pkg/machinery/config/config" "github.com/siderolabs/talos/pkg/machinery/constants" "github.com/siderolabs/talos/pkg/machinery/nethelpers" "github.com/siderolabs/talos/pkg/machinery/resources/config" @@ -71,8 +71,6 @@ func (ctrl *HostDNSConfigController) Run(ctx context.Context, r controller.Runti case <-r.EventCh(): } - var cfgProvider talosconfig.Config - r.StartTrackingOutputs() cfg, err := safe.ReaderGetByID[*config.MachineConfig](ctx, r, config.ActiveID) @@ -80,8 +78,12 @@ func (ctrl *HostDNSConfigController) Run(ctx context.Context, r controller.Runti if !state.IsNotFoundError(err) { return fmt.Errorf("error getting config: %w", err) } - } else if cfg.Config().Machine() != nil { - cfgProvider = cfg.Config() + } + + var hostDNSConfig cfgcfg.NetworkHostDNSConfig + + if cfg != nil { + hostDNSConfig = cfg.Config().NetworkHostDNSConfig() } newServiceAddrs := make([]netip.Addr, 0, 2) @@ -93,21 +95,27 @@ func (ctrl *HostDNSConfigController) Run(ctx context.Context, r controller.Runti res.TypedSpec().ServiceHostDNSAddress = netip.Addr{} - if cfgProvider == nil { + if hostDNSConfig == nil { res.TypedSpec().Enabled = false return nil } - res.TypedSpec().Enabled = cfgProvider.Machine().Features().HostDNS().Enabled() - res.TypedSpec().ResolveMemberNames = cfgProvider.Machine().Features().HostDNS().ResolveMemberNames() + res.TypedSpec().Enabled = hostDNSConfig.HostDNSEnabled() + res.TypedSpec().ResolveMemberNames = hostDNSConfig.ResolveMemberNames() - if !cfgProvider.Machine().Features().HostDNS().ForwardKubeDNSToHost() { + if !hostDNSConfig.ForwardKubeDNSToHost() { return nil } + var podCIDRs []string + + if cfg.Config().Cluster() != nil { + podCIDRs = cfg.Config().Cluster().Network().PodCIDRs() + } + if slices.ContainsFunc( - cfgProvider.Cluster().Network().PodCIDRs(), + podCIDRs, func(cidr string) bool { return netip.MustParsePrefix(cidr).Addr().Is4() }, ) { parsed := netip.MustParseAddr(constants.HostDNSAddress) diff --git a/internal/app/machined/pkg/controllers/network/hostdns_config_test.go b/internal/app/machined/pkg/controllers/network/hostdns_config_test.go new file mode 100644 index 000000000..ac0467492 --- /dev/null +++ b/internal/app/machined/pkg/controllers/network/hostdns_config_test.go @@ -0,0 +1,263 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package network_test + +import ( + "net/netip" + "net/url" + "testing" + "time" + + "github.com/cosi-project/runtime/pkg/resource/rtestutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + + "github.com/siderolabs/talos/internal/app/machined/pkg/controllers/ctest" + netctrl "github.com/siderolabs/talos/internal/app/machined/pkg/controllers/network" + "github.com/siderolabs/talos/pkg/machinery/config/container" + networkcfg "github.com/siderolabs/talos/pkg/machinery/config/types/network" + "github.com/siderolabs/talos/pkg/machinery/config/types/v1alpha1" + "github.com/siderolabs/talos/pkg/machinery/constants" + "github.com/siderolabs/talos/pkg/machinery/nethelpers" + "github.com/siderolabs/talos/pkg/machinery/resources/config" + "github.com/siderolabs/talos/pkg/machinery/resources/network" +) + +type HostDNSConfigSuite struct { + ctest.DefaultSuite +} + +func (suite *HostDNSConfigSuite) TestNoConfig() { + ctest.AssertResource(suite, network.HostDNSConfigID, func(r *network.HostDNSConfig, asrt *assert.Assertions) { + asrt.False(r.TypedSpec().Enabled) + asrt.Equal( + []netip.AddrPort{netip.MustParseAddrPort("127.0.0.53:53")}, + r.TypedSpec().ListenAddresses, + ) + asrt.Equal(netip.Addr{}, r.TypedSpec().ServiceHostDNSAddress) + asrt.False(r.TypedSpec().ResolveMemberNames) + }) +} + +func (suite *HostDNSConfigSuite) TestLegacyConfigEnabled() { + u, err := url.Parse("https://foo:6443") + suite.Require().NoError(err) + + cfg := config.NewMachineConfig( + container.NewV1Alpha1( + &v1alpha1.Config{ + ConfigVersion: "v1alpha1", + MachineConfig: &v1alpha1.MachineConfig{ + MachineFeatures: &v1alpha1.FeaturesConfig{ + HostDNSSupport: &v1alpha1.HostDNSConfig{ //nolint:staticcheck // testing legacy config + HostDNSConfigEnabled: new(true), + HostDNSResolveMemberNames: new(true), + }, + }, + }, + ClusterConfig: &v1alpha1.ClusterConfig{ + ControlPlane: &v1alpha1.ControlPlaneConfig{ + Endpoint: &v1alpha1.Endpoint{URL: u}, + }, + ClusterNetwork: &v1alpha1.ClusterNetworkConfig{ + PodSubnet: []string{constants.DefaultIPv4PodNet}, + }, + }, + }, + ), + ) + + suite.Create(cfg) + + ctest.AssertResource(suite, network.HostDNSConfigID, func(r *network.HostDNSConfig, asrt *assert.Assertions) { + asrt.True(r.TypedSpec().Enabled) + asrt.Equal( + []netip.AddrPort{netip.MustParseAddrPort("127.0.0.53:53")}, + r.TypedSpec().ListenAddresses, + ) + asrt.Equal(netip.Addr{}, r.TypedSpec().ServiceHostDNSAddress) + asrt.True(r.TypedSpec().ResolveMemberNames) + }) + + ctest.AssertNoResource[*network.AddressSpec]( + suite, + network.LayeredID(network.ConfigOperator, network.AddressID("lo", netip.MustParsePrefix(constants.HostDNSAddress+"/32"))), + rtestutils.WithNamespace(network.ConfigNamespaceName), + ) +} + +func (suite *HostDNSConfigSuite) TestLegacyConfigForwardKubeDNSIPv4() { + u, err := url.Parse("https://foo:6443") + suite.Require().NoError(err) + + cfg := config.NewMachineConfig( + container.NewV1Alpha1( + &v1alpha1.Config{ + ConfigVersion: "v1alpha1", + MachineConfig: &v1alpha1.MachineConfig{ + MachineFeatures: &v1alpha1.FeaturesConfig{ + HostDNSSupport: &v1alpha1.HostDNSConfig{ //nolint:staticcheck // testing legacy config + HostDNSConfigEnabled: new(true), + HostDNSForwardKubeDNSToHost: new(true), + }, + }, + }, + ClusterConfig: &v1alpha1.ClusterConfig{ + ControlPlane: &v1alpha1.ControlPlaneConfig{ + Endpoint: &v1alpha1.Endpoint{URL: u}, + }, + ClusterNetwork: &v1alpha1.ClusterNetworkConfig{ + PodSubnet: []string{constants.DefaultIPv4PodNet, constants.DefaultIPv6PodNet}, + }, + }, + }, + ), + ) + + suite.Create(cfg) + + hostDNSAddr := netip.MustParseAddr(constants.HostDNSAddress) + + ctest.AssertResource(suite, network.HostDNSConfigID, func(r *network.HostDNSConfig, asrt *assert.Assertions) { + asrt.True(r.TypedSpec().Enabled) + asrt.Equal( + []netip.AddrPort{ + netip.MustParseAddrPort("127.0.0.53:53"), + netip.AddrPortFrom(hostDNSAddr, 53), + }, + r.TypedSpec().ListenAddresses, + ) + asrt.Equal(hostDNSAddr, r.TypedSpec().ServiceHostDNSAddress) + }) + + addrPrefix := netip.PrefixFrom(hostDNSAddr, hostDNSAddr.BitLen()) + + ctest.AssertResource( + suite, + network.LayeredID(network.ConfigOperator, network.AddressID("lo", addrPrefix)), + func(r *network.AddressSpec, asrt *assert.Assertions) { + spec := r.TypedSpec() + + asrt.Equal(addrPrefix, spec.Address) + asrt.Equal("lo", spec.LinkName) + asrt.Equal(nethelpers.FamilyInet4, spec.Family) + asrt.Equal(nethelpers.ScopeHost, spec.Scope) + asrt.Equal(nethelpers.AddressFlags(nethelpers.AddressPermanent), spec.Flags) + asrt.Equal(network.ConfigOperator, spec.ConfigLayer) + }, + rtestutils.WithNamespace(network.ConfigNamespaceName), + ) +} + +func (suite *HostDNSConfigSuite) TestLegacyConfigForwardKubeDNSIPv6Only() { + u, err := url.Parse("https://foo:6443") + suite.Require().NoError(err) + + cfg := config.NewMachineConfig( + container.NewV1Alpha1( + &v1alpha1.Config{ + ConfigVersion: "v1alpha1", + MachineConfig: &v1alpha1.MachineConfig{ + MachineFeatures: &v1alpha1.FeaturesConfig{ + HostDNSSupport: &v1alpha1.HostDNSConfig{ //nolint:staticcheck // testing legacy config + HostDNSConfigEnabled: new(true), + HostDNSForwardKubeDNSToHost: new(true), + }, + }, + }, + ClusterConfig: &v1alpha1.ClusterConfig{ + ControlPlane: &v1alpha1.ControlPlaneConfig{ + Endpoint: &v1alpha1.Endpoint{URL: u}, + }, + ClusterNetwork: &v1alpha1.ClusterNetworkConfig{ + PodSubnet: []string{constants.DefaultIPv6PodNet}, + }, + }, + }, + ), + ) + + suite.Create(cfg) + + ctest.AssertResource(suite, network.HostDNSConfigID, func(r *network.HostDNSConfig, asrt *assert.Assertions) { + asrt.True(r.TypedSpec().Enabled) + asrt.Equal( + []netip.AddrPort{netip.MustParseAddrPort("127.0.0.53:53")}, + r.TypedSpec().ListenAddresses, + ) + asrt.Equal(netip.Addr{}, r.TypedSpec().ServiceHostDNSAddress) + }) + + ctest.AssertNoResource[*network.AddressSpec]( + suite, + network.LayeredID(network.ConfigOperator, network.AddressID("lo", netip.MustParsePrefix(constants.HostDNSAddress+"/32"))), + rtestutils.WithNamespace(network.ConfigNamespaceName), + ) +} + +func (suite *HostDNSConfigSuite) TestResolverConfigDocument() { + rc := networkcfg.NewResolverConfigV1Alpha1() + rc.ResolverHostDNS = networkcfg.HostDNSConfig{ + HostDNSEnabled: new(true), + HostDNSForwardKubeDNSToHost: new(true), + HostDNSResolveMemberNames: new(true), + } + + v1 := &v1alpha1.Config{ + ConfigVersion: "v1alpha1", + MachineConfig: &v1alpha1.MachineConfig{}, + ClusterConfig: &v1alpha1.ClusterConfig{ + ClusterNetwork: &v1alpha1.ClusterNetworkConfig{ + PodSubnet: []string{constants.DefaultIPv4PodNet}, + }, + }, + } + + ctr, err := container.New(v1, rc) + suite.Require().NoError(err) + + suite.Create(config.NewMachineConfig(ctr)) + + hostDNSAddr := netip.MustParseAddr(constants.HostDNSAddress) + + ctest.AssertResource(suite, network.HostDNSConfigID, func(r *network.HostDNSConfig, asrt *assert.Assertions) { + asrt.True(r.TypedSpec().Enabled) + asrt.True(r.TypedSpec().ResolveMemberNames) + asrt.Equal( + []netip.AddrPort{ + netip.MustParseAddrPort("127.0.0.53:53"), + netip.AddrPortFrom(hostDNSAddr, 53), + }, + r.TypedSpec().ListenAddresses, + ) + asrt.Equal(hostDNSAddr, r.TypedSpec().ServiceHostDNSAddress) + }) + + addrPrefix := netip.PrefixFrom(hostDNSAddr, hostDNSAddr.BitLen()) + + ctest.AssertResource( + suite, + network.LayeredID(network.ConfigOperator, network.AddressID("lo", addrPrefix)), + func(r *network.AddressSpec, asrt *assert.Assertions) { + asrt.Equal(addrPrefix, r.TypedSpec().Address) + asrt.Equal(nethelpers.FamilyInet4, r.TypedSpec().Family) + asrt.Equal("lo", r.TypedSpec().LinkName) + }, + rtestutils.WithNamespace(network.ConfigNamespaceName), + ) +} + +func TestHostDNSConfigSuite(t *testing.T) { + t.Parallel() + + suite.Run(t, &HostDNSConfigSuite{ + DefaultSuite: ctest.DefaultSuite{ + Timeout: 5 * time.Second, + AfterSetup: func(s *ctest.DefaultSuite) { + s.Require().NoError(s.Runtime().RegisterController(&netctrl.HostDNSConfigController{})) + }, + }, + }) +} diff --git a/internal/app/machined/pkg/controllers/network/nftables_chain_config.go b/internal/app/machined/pkg/controllers/network/nftables_chain_config.go index d238899d5..89a82bd82 100644 --- a/internal/app/machined/pkg/controllers/network/nftables_chain_config.go +++ b/internal/app/machined/pkg/controllers/network/nftables_chain_config.go @@ -207,8 +207,8 @@ func (ctrl *NfTablesChainConfigController) buildIngressChain(cfg *config.Machine }, ) - if cfg.Config().Machine() != nil && cfg.Config().Cluster() != nil { - if cfg.Config().Machine().Features().HostDNS().ForwardKubeDNSToHost() { + if hostDNSConfig := cfg.Config().NetworkHostDNSConfig(); hostDNSConfig != nil { + if hostDNSConfig.ForwardKubeDNSToHost() { hostDNSIP := netip.MustParseAddr(constants.HostDNSAddress) // allow traffic to host DNS diff --git a/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_runtime.go b/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_runtime.go index 3d8b73688..b740c3f8c 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_runtime.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/v1alpha1_runtime.go @@ -198,7 +198,7 @@ func (r *Runtime) CanApplyImmediate(cfg config.Provider) error { if newConfig.MachineConfig.MachineFeatures != nil && currentConfig.MachineConfig.MachineFeatures != nil { newConfig.MachineConfig.MachineFeatures.KubernetesTalosAPIAccessConfig = currentConfig.MachineConfig.MachineFeatures.KubernetesTalosAPIAccessConfig newConfig.MachineConfig.MachineFeatures.KubePrismSupport = currentConfig.MachineConfig.MachineFeatures.KubePrismSupport - newConfig.MachineConfig.MachineFeatures.HostDNSSupport = currentConfig.MachineConfig.MachineFeatures.HostDNSSupport + newConfig.MachineConfig.MachineFeatures.HostDNSSupport = currentConfig.MachineConfig.MachineFeatures.HostDNSSupport //nolint:staticcheck // backwards compatibility newConfig.MachineConfig.MachineFeatures.ImageCacheSupport = currentConfig.MachineConfig.MachineFeatures.ImageCacheSupport newConfig.MachineConfig.MachineFeatures.FeatureNodeAddressSortAlgorithm = currentConfig.MachineConfig.MachineFeatures.FeatureNodeAddressSortAlgorithm } diff --git a/pkg/machinery/config/config/config.go b/pkg/machinery/config/config/config.go index bad5f5fc3..80e151736 100644 --- a/pkg/machinery/config/config/config.go +++ b/pkg/machinery/config/config/config.go @@ -22,6 +22,7 @@ type Config interface { //nolint:interfacebloat NetworkStaticHostConfig() []NetworkStaticHostConfig NetworkHostnameConfig() NetworkHostnameConfig NetworkResolverConfig() NetworkResolverConfig + NetworkHostDNSConfig() NetworkHostDNSConfig NetworkTimeSyncConfig() NetworkTimeSyncConfig NetworkKubeSpanConfig() NetworkKubeSpanConfig NetworkCommonLinkConfigs() []NetworkCommonLinkConfig diff --git a/pkg/machinery/config/config/machine.go b/pkg/machinery/config/config/machine.go index 17310ba0c..e21b1883f 100644 --- a/pkg/machinery/config/config/machine.go +++ b/pkg/machinery/config/config/machine.go @@ -378,7 +378,6 @@ type SystemDiskEncryption interface { type Features interface { KubernetesTalosAPIAccess() KubernetesTalosAPIAccess DiskQuotaSupportEnabled() bool - HostDNS() HostDNS KubePrism() KubePrism ImageCache() ImageCache NodeAddressSortAlgorithm() nethelpers.AddressSortAlgorithm @@ -397,13 +396,6 @@ type KubePrism interface { Port() int } -// HostDNS describes the host DNS configuration. -type HostDNS interface { - Enabled() bool - ForwardKubeDNSToHost() bool - ResolveMemberNames() bool -} - // ImageCache describes the image cache configuration. type ImageCache interface { LocalEnabled() bool diff --git a/pkg/machinery/config/config/network.go b/pkg/machinery/config/config/network.go index 4c2bf10b1..0e3f4b075 100644 --- a/pkg/machinery/config/config/network.go +++ b/pkg/machinery/config/config/network.go @@ -381,3 +381,10 @@ type NetworkRoutingRuleConfig interface { FwMark() uint32 FwMask() uint32 } + +// NetworkHostDNSConfig defines a host DNS configuration. +type NetworkHostDNSConfig interface { + HostDNSEnabled() bool + ForwardKubeDNSToHost() bool + ResolveMemberNames() bool +} diff --git a/pkg/machinery/config/container/container.go b/pkg/machinery/config/container/container.go index b01eb7d70..c4215a09c 100644 --- a/pkg/machinery/config/container/container.go +++ b/pkg/machinery/config/container/container.go @@ -7,21 +7,17 @@ package container import ( "bytes" - "context" "errors" "fmt" "slices" "strings" - "github.com/cosi-project/runtime/pkg/state" - "github.com/hashicorp/go-multierror" "github.com/siderolabs/gen/xslices" coreconfig "github.com/siderolabs/talos/pkg/machinery/config" "github.com/siderolabs/talos/pkg/machinery/config/config" "github.com/siderolabs/talos/pkg/machinery/config/encoder" "github.com/siderolabs/talos/pkg/machinery/config/types/v1alpha1" - "github.com/siderolabs/talos/pkg/machinery/config/validation" ) // V1Alpha1ConflictValidator is the interface implemented by config documents which conflict with legacy v1alpha1 config. @@ -328,6 +324,23 @@ func (container *Container) NetworkResolverConfig() config.NetworkResolverConfig return nil } +// NetworkHostDNSConfig implements config.Config interface. +func (container *Container) NetworkHostDNSConfig() config.NetworkHostDNSConfig { + // first check if we have a dedicated document, and it is not empty + // for backwards compatibility, we will fall back to v1alpha1 if the ResolverConfig document does not have hostDNS enabled + matching := findMatchingDocs[config.NetworkHostDNSConfig](container.documents) + if len(matching) > 0 && matching[0].HostDNSEnabled() { + return matching[0] + } + + // fallback to v1alpha1 + if container.v1alpha1Config != nil { + return container.v1alpha1Config.NetworkHostDNSConfig() + } + + return nil +} + // NetworkTimeSyncConfig implements config.Config interface. func (container *Container) NetworkTimeSyncConfig() config.NetworkTimeSyncConfig { // first check if we have a dedicated document @@ -577,90 +590,6 @@ func docID(doc config.Document) string { return id } -// Validate checks configuration and returns warnings and fatal errors (as multierror). -// -//nolint:gocyclo -func (container *Container) Validate(mode validation.RuntimeMode, opt ...validation.Option) ([]string, error) { - var ( - warnings []string - err error - ) - - if container.v1alpha1Config != nil { - warnings, err = container.v1alpha1Config.Validate(mode, opt...) - if err != nil { - err = fmt.Errorf("v1alpha1.Config: %w", err) - } - } - - var multiErr *multierror.Error - - if err != nil { - multiErr = multierror.Append(multiErr, err) - } - - for _, doc := range container.documents { - if validatableDoc, ok := doc.(config.Validator); ok { - docWarnings, docErr := validatableDoc.Validate(mode, opt...) - if docErr != nil { - docErr = fmt.Errorf("%s: %w", docID(doc), docErr) - } - - warnings = append(warnings, docWarnings...) - multiErr = multierror.Append(multiErr, docErr) - } - } - - // now cross-validate the config - if container.v1alpha1Config != nil { - for _, doc := range container.documents { - if conflictValidator, ok := doc.(V1Alpha1ConflictValidator); ok { - err := conflictValidator.V1Alpha1ConflictValidate(container.v1alpha1Config) - if err != nil { - multiErr = multierror.Append(multiErr, err) - } - } - } - } - - return warnings, multiErr.ErrorOrNil() -} - -// RuntimeValidate validates the config in the runtime context. -func (container *Container) RuntimeValidate(ctx context.Context, st state.State, mode validation.RuntimeMode, opt ...validation.Option) ([]string, error) { - var ( - warnings []string - err error - ) - - if container.v1alpha1Config != nil { - warnings, err = container.v1alpha1Config.RuntimeValidate(ctx, st, mode, opt...) - if err != nil { - err = fmt.Errorf("v1alpha1.Config: %w", err) - } - } - - var multiErr *multierror.Error - - if err != nil { - multiErr = multierror.Append(multiErr, err) - } - - for _, doc := range container.documents { - if validatableDoc, ok := doc.(config.RuntimeValidator); ok { - docWarnings, docErr := validatableDoc.RuntimeValidate(ctx, st, mode, opt...) - if docErr != nil { - docErr = fmt.Errorf("%s: %w", docID(doc), docErr) - } - - warnings = append(warnings, docWarnings...) - multiErr = multierror.Append(multiErr, docErr) - } - } - - return warnings, multiErr.ErrorOrNil() -} - // RedactSecrets returns a copy of the Provider with all secrets replaced with the given string. func (container *Container) RedactSecrets(replacement string) coreconfig.Provider { clone := container.clone() diff --git a/pkg/machinery/config/container/container_test.go b/pkg/machinery/config/container/container_test.go index afac4ba7d..697deec46 100644 --- a/pkg/machinery/config/container/container_test.go +++ b/pkg/machinery/config/container/container_test.go @@ -8,7 +8,6 @@ import ( "net/url" "testing" - "github.com/siderolabs/crypto/x509" "github.com/siderolabs/gen/xtesting/must" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -23,8 +22,6 @@ import ( "github.com/siderolabs/talos/pkg/machinery/config/types/runtime/extensions" "github.com/siderolabs/talos/pkg/machinery/config/types/siderolink" "github.com/siderolabs/talos/pkg/machinery/config/types/v1alpha1" - "github.com/siderolabs/talos/pkg/machinery/constants" - blockres "github.com/siderolabs/talos/pkg/machinery/resources/block" ) func TestNew(t *testing.T) { @@ -153,197 +150,6 @@ func TestPatchV1Alpha1(t *testing.T) { assert.Equal(t, "https://siderolink.api/?jointoken=secret&user=alice", patchedCfg.SideroLink().APIUrl().String()) } -func TestValidate(t *testing.T) { - t.Parallel() - - sideroLinkCfg := siderolink.NewConfigV1Alpha1() - sideroLinkCfg.APIUrlConfig.URL = must.Value(url.Parse("https://siderolink.api/?jointoken=secret&user=alice"))(t) - - invalidSideroLinkCfg := siderolink.NewConfigV1Alpha1() - - v1alpha1Cfg := &v1alpha1.Config{ - ClusterConfig: &v1alpha1.ClusterConfig{ - ControlPlane: &v1alpha1.ControlPlaneConfig{ - Endpoint: &v1alpha1.Endpoint{ - URL: must.Value(url.Parse("https://localhost:6443"))(t), - }, - }, - }, - MachineConfig: &v1alpha1.MachineConfig{ - MachineType: "worker", - MachineCA: &x509.PEMEncodedCertificateAndKey{ - Crt: []byte("cert"), - }, - }, - } - - invalidV1alpha1Config := &v1alpha1.Config{} - - for _, tt := range []struct { - name string - documents []config.Document - - expectedError string - expecetedWarnings []string - }{ - { - name: "empty", - }, - { - name: "multi-doc", - documents: []config.Document{sideroLinkCfg, v1alpha1Cfg}, - }, - { - name: "only siderolink", - documents: []config.Document{sideroLinkCfg}, - }, - { - name: "only v1alpha1", - documents: []config.Document{v1alpha1Cfg}, - }, - { - name: "invalid siderolink", - documents: []config.Document{invalidSideroLinkCfg}, - expectedError: "1 error occurred:\n\t* SideroLinkConfig: apiUrl is required\n\n", - }, - { - name: "invalid v1alpha1", - documents: []config.Document{invalidV1alpha1Config}, - expectedError: "1 error occurred:\n\t* v1alpha1.Config: 1 error occurred:\n\t* machine instructions are required\n\n\n\n", - }, - { - name: "invalid multi-doc", - documents: []config.Document{invalidSideroLinkCfg, invalidV1alpha1Config}, - expectedError: "2 errors occurred:\n\t* v1alpha1.Config: 1 error occurred:\n\t* machine instructions are required\n\n\n\t* SideroLinkConfig: apiUrl is required\n\n", - }, - } { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - ctr, err := container.New(tt.documents...) - require.NoError(t, err) - - warnings, err := ctr.Validate(validationMode{}) - - if tt.expectedError == "" { - require.NoError(t, err) - } else { - require.EqualError(t, err, tt.expectedError) - } - - require.Equal(t, tt.expecetedWarnings, warnings) - }) - } -} - -func TestCrossValidateEncryption(t *testing.T) { - t.Parallel() - - v1alpha1Cfg := &v1alpha1.Config{ - ClusterConfig: &v1alpha1.ClusterConfig{ - ControlPlane: &v1alpha1.ControlPlaneConfig{ - Endpoint: &v1alpha1.Endpoint{ - URL: must.Value(url.Parse("https://localhost:6443"))(t), - }, - }, - }, - MachineConfig: &v1alpha1.MachineConfig{ - MachineType: "worker", - MachineCA: &x509.PEMEncodedCertificateAndKey{ - Crt: []byte("cert"), - }, - MachineSystemDiskEncryption: &v1alpha1.SystemDiskEncryptionConfig{ - EphemeralPartition: &v1alpha1.EncryptionConfig{ - EncryptionKeys: []*v1alpha1.EncryptionKey{ - { - KeySlot: 1, - KeyStatic: &v1alpha1.EncryptionKeyStatic{ - KeyData: "static-key", - }, - }, - }, - }, - }, - }, - } - - defaultEphemeral := block.NewVolumeConfigV1Alpha1() - defaultEphemeral.MetaName = constants.EphemeralPartitionLabel - - encryptedEphemeral := block.NewVolumeConfigV1Alpha1() - encryptedEphemeral.MetaName = constants.EphemeralPartitionLabel - encryptedEphemeral.EncryptionSpec = block.EncryptionSpec{ - EncryptionProvider: blockres.EncryptionProviderLUKS2, - EncryptionKeys: []block.EncryptionKey{ - { - KeySlot: 2, - KeyStatic: &block.EncryptionKeyStatic{ - KeyData: "encrypted-static-key", - }, - }, - }, - } - - encryptedState := block.NewVolumeConfigV1Alpha1() - encryptedState.MetaName = constants.StatePartitionLabel - encryptedState.EncryptionSpec = block.EncryptionSpec{ - EncryptionProvider: blockres.EncryptionProviderLUKS2, - EncryptionKeys: []block.EncryptionKey{ - { - KeySlot: 3, - KeyTPM: &block.EncryptionKeyTPM{}, - }, - }, - } - - for _, tt := range []struct { - name string - documents []config.Document - - expectedError string - expecetedWarnings []string - }{ - { - name: "only v1alpha1", - documents: []config.Document{v1alpha1Cfg}, - }, - { - name: "v1alpha1 with no-conflict volumes", - documents: []config.Document{v1alpha1Cfg, defaultEphemeral, encryptedState}, - }, - { - name: "v1alpha1 with no-conflict volumes", - documents: []config.Document{v1alpha1Cfg, encryptedState}, - }, - { - name: "no v1alpha1", - documents: []config.Document{encryptedEphemeral, encryptedState}, - }, - { - name: "conflict on ephemeral encryption", - documents: []config.Document{v1alpha1Cfg, encryptedEphemeral}, - expectedError: "1 error occurred:\n\t* system disk encryption for \"EPHEMERAL\" is configured in both v1alpha1.Config and VolumeConfig\n\n", - }, - } { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - ctr, err := container.New(tt.documents...) - require.NoError(t, err) - - warnings, err := ctr.Validate(validationMode{}) - - if tt.expectedError == "" { - require.NoError(t, err) - } else { - require.EqualError(t, err, tt.expectedError) - } - - require.Equal(t, tt.expecetedWarnings, warnings) - }) - } -} - func TestRunDefaultDHCPOperators(t *testing.T) { t.Parallel() @@ -397,17 +203,3 @@ func TestRunDefaultDHCPOperators(t *testing.T) { }) } } - -type validationMode struct{} - -func (validationMode) String() string { - return "" -} - -func (validationMode) RequiresInstall() bool { - return false -} - -func (validationMode) InContainer() bool { - return false -} diff --git a/pkg/machinery/config/container/validate.go b/pkg/machinery/config/container/validate.go new file mode 100644 index 000000000..1c4e04c93 --- /dev/null +++ b/pkg/machinery/config/container/validate.go @@ -0,0 +1,134 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package container + +import ( + "context" + "fmt" + + "github.com/cosi-project/runtime/pkg/state" + "github.com/hashicorp/go-multierror" + + "github.com/siderolabs/talos/pkg/machinery/config" + "github.com/siderolabs/talos/pkg/machinery/config/validation" +) + +// Validate checks configuration and returns warnings and fatal errors (as multierror). +// +// The validation first validates each individual document, then it does conflict validation of new +// documents with v1alpha1.Config (if it exists). +// Finally, whole container is validated according to the mode. +// +//nolint:gocyclo +func (container *Container) Validate(mode validation.RuntimeMode, opt ...validation.Option) ([]string, error) { + var ( + warnings []string + err error + ) + + if container.v1alpha1Config != nil { + warnings, err = container.v1alpha1Config.Validate(mode, opt...) + if err != nil { + err = fmt.Errorf("v1alpha1.Config: %w", err) + } + } + + var multiErr *multierror.Error + + if err != nil { + multiErr = multierror.Append(multiErr, err) + } + + for _, doc := range container.documents { + if validatableDoc, ok := doc.(config.Validator); ok { + docWarnings, docErr := validatableDoc.Validate(mode, opt...) + if docErr != nil { + docErr = fmt.Errorf("%s: %w", docID(doc), docErr) + } + + warnings = append(warnings, docWarnings...) + multiErr = multierror.Append(multiErr, docErr) + } + } + + // now cross-validate the config + if container.v1alpha1Config != nil { + for _, doc := range container.documents { + if conflictValidator, ok := doc.(V1Alpha1ConflictValidator); ok { + err := conflictValidator.V1Alpha1ConflictValidate(container.v1alpha1Config) + if err != nil { + multiErr = multierror.Append(multiErr, err) + } + } + } + } + + if err := container.validateContainer(mode); err != nil { + multiErr = multierror.Append(multiErr, err) + } + + return warnings, multiErr.ErrorOrNil() +} + +// RuntimeValidate validates the config in the runtime context. +func (container *Container) RuntimeValidate(ctx context.Context, st state.State, mode validation.RuntimeMode, opt ...validation.Option) ([]string, error) { + var ( + warnings []string + err error + ) + + if container.v1alpha1Config != nil { + warnings, err = container.v1alpha1Config.RuntimeValidate(ctx, st, mode, opt...) + if err != nil { + err = fmt.Errorf("v1alpha1.Config: %w", err) + } + } + + var multiErr *multierror.Error + + if err != nil { + multiErr = multierror.Append(multiErr, err) + } + + for _, doc := range container.documents { + if validatableDoc, ok := doc.(config.RuntimeValidator); ok { + docWarnings, docErr := validatableDoc.RuntimeValidate(ctx, st, mode, opt...) + if docErr != nil { + docErr = fmt.Errorf("%s: %w", docID(doc), docErr) + } + + warnings = append(warnings, docWarnings...) + multiErr = multierror.Append(multiErr, docErr) + } + } + + return warnings, multiErr.ErrorOrNil() +} + +// validateContainer validates the full configuration container. +// +// This validation is used to do validation which only makes sense for the full configuration (vs. individual documents). +func (container *Container) validateContainer(mode validation.RuntimeMode) error { + var errs error + + if mode.InContainer() { + // in container mode, HostDNS must be enabled and forward KubeDNS to host must be enabled as well + hostDNSConfig := container.NetworkHostDNSConfig() + + if hostDNSConfig == nil { + errs = multierror.Append(errs, fmt.Errorf("hostDNS config is required in container mode")) + } else { + if !hostDNSConfig.HostDNSEnabled() { + errs = multierror.Append(errs, fmt.Errorf("hostDNS must be enabled in container mode")) + } + + if !hostDNSConfig.ForwardKubeDNSToHost() { + errs = multierror.Append(errs, fmt.Errorf("forwardKubeDNSToHost must be enabled in container mode")) + } + } + } + + return errs +} diff --git a/pkg/machinery/config/container/validate_test.go b/pkg/machinery/config/container/validate_test.go new file mode 100644 index 000000000..d4178e6a5 --- /dev/null +++ b/pkg/machinery/config/container/validate_test.go @@ -0,0 +1,344 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package container_test + +import ( + "net/netip" + "net/url" + "testing" + + "github.com/siderolabs/crypto/x509" + "github.com/siderolabs/gen/xtesting/must" + "github.com/stretchr/testify/require" + + "github.com/siderolabs/talos/pkg/machinery/config/config" + "github.com/siderolabs/talos/pkg/machinery/config/container" + "github.com/siderolabs/talos/pkg/machinery/config/types/block" + "github.com/siderolabs/talos/pkg/machinery/config/types/network" + "github.com/siderolabs/talos/pkg/machinery/config/types/siderolink" + "github.com/siderolabs/talos/pkg/machinery/config/types/v1alpha1" + "github.com/siderolabs/talos/pkg/machinery/constants" + blockres "github.com/siderolabs/talos/pkg/machinery/resources/block" +) + +func TestValidate(t *testing.T) { + t.Parallel() + + sideroLinkCfg := siderolink.NewConfigV1Alpha1() + sideroLinkCfg.APIUrlConfig.URL = must.Value(url.Parse("https://siderolink.api/?jointoken=secret&user=alice"))(t) + + invalidSideroLinkCfg := siderolink.NewConfigV1Alpha1() + + v1alpha1Cfg := &v1alpha1.Config{ + ClusterConfig: &v1alpha1.ClusterConfig{ + ControlPlane: &v1alpha1.ControlPlaneConfig{ + Endpoint: &v1alpha1.Endpoint{ + URL: must.Value(url.Parse("https://localhost:6443"))(t), + }, + }, + }, + MachineConfig: &v1alpha1.MachineConfig{ + MachineType: "worker", + MachineCA: &x509.PEMEncodedCertificateAndKey{ + Crt: []byte("cert"), + }, + }, + } + + invalidV1alpha1Config := &v1alpha1.Config{} + + for _, tt := range []struct { + name string + documents []config.Document + + expectedError string + expectedWarnings []string + }{ + { + name: "empty", + }, + { + name: "multi-doc", + documents: []config.Document{sideroLinkCfg, v1alpha1Cfg}, + }, + { + name: "only siderolink", + documents: []config.Document{sideroLinkCfg}, + }, + { + name: "only v1alpha1", + documents: []config.Document{v1alpha1Cfg}, + }, + { + name: "invalid siderolink", + documents: []config.Document{invalidSideroLinkCfg}, + expectedError: "1 error occurred:\n\t* SideroLinkConfig: apiUrl is required\n\n", + }, + { + name: "invalid v1alpha1", + documents: []config.Document{invalidV1alpha1Config}, + expectedError: "1 error occurred:\n\t* v1alpha1.Config: 1 error occurred:\n\t* machine instructions are required\n\n\n\n", + }, + { + name: "invalid multi-doc", + documents: []config.Document{invalidSideroLinkCfg, invalidV1alpha1Config}, + expectedError: "2 errors occurred:\n\t* v1alpha1.Config: 1 error occurred:\n\t* machine instructions are required\n\n\n\t* SideroLinkConfig: apiUrl is required\n\n", + }, + } { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctr, err := container.New(tt.documents...) + require.NoError(t, err) + + warnings, err := ctr.Validate(validationMode{}) + + if tt.expectedError == "" { + require.NoError(t, err) + } else { + require.EqualError(t, err, tt.expectedError) + } + + require.Equal(t, tt.expectedWarnings, warnings) + }) + } +} + +func TestCrossValidateEncryption(t *testing.T) { + t.Parallel() + + v1alpha1Cfg := &v1alpha1.Config{ + ClusterConfig: &v1alpha1.ClusterConfig{ + ControlPlane: &v1alpha1.ControlPlaneConfig{ + Endpoint: &v1alpha1.Endpoint{ + URL: must.Value(url.Parse("https://localhost:6443"))(t), + }, + }, + }, + MachineConfig: &v1alpha1.MachineConfig{ + MachineType: "worker", + MachineCA: &x509.PEMEncodedCertificateAndKey{ + Crt: []byte("cert"), + }, + MachineSystemDiskEncryption: &v1alpha1.SystemDiskEncryptionConfig{ + EphemeralPartition: &v1alpha1.EncryptionConfig{ + EncryptionKeys: []*v1alpha1.EncryptionKey{ + { + KeySlot: 1, + KeyStatic: &v1alpha1.EncryptionKeyStatic{ + KeyData: "static-key", + }, + }, + }, + }, + }, + }, + } + + defaultEphemeral := block.NewVolumeConfigV1Alpha1() + defaultEphemeral.MetaName = constants.EphemeralPartitionLabel + + encryptedEphemeral := block.NewVolumeConfigV1Alpha1() + encryptedEphemeral.MetaName = constants.EphemeralPartitionLabel + encryptedEphemeral.EncryptionSpec = block.EncryptionSpec{ + EncryptionProvider: blockres.EncryptionProviderLUKS2, + EncryptionKeys: []block.EncryptionKey{ + { + KeySlot: 2, + KeyStatic: &block.EncryptionKeyStatic{ + KeyData: "encrypted-static-key", + }, + }, + }, + } + + encryptedState := block.NewVolumeConfigV1Alpha1() + encryptedState.MetaName = constants.StatePartitionLabel + encryptedState.EncryptionSpec = block.EncryptionSpec{ + EncryptionProvider: blockres.EncryptionProviderLUKS2, + EncryptionKeys: []block.EncryptionKey{ + { + KeySlot: 3, + KeyTPM: &block.EncryptionKeyTPM{}, + }, + }, + } + + for _, tt := range []struct { + name string + documents []config.Document + + expectedError string + expectedWarnings []string + }{ + { + name: "only v1alpha1", + documents: []config.Document{v1alpha1Cfg}, + }, + { + name: "v1alpha1 with no-conflict volumes", + documents: []config.Document{v1alpha1Cfg, defaultEphemeral, encryptedState}, + }, + { + name: "v1alpha1 with no-conflict volumes", + documents: []config.Document{v1alpha1Cfg, encryptedState}, + }, + { + name: "no v1alpha1", + documents: []config.Document{encryptedEphemeral, encryptedState}, + }, + { + name: "conflict on ephemeral encryption", + documents: []config.Document{v1alpha1Cfg, encryptedEphemeral}, + expectedError: "1 error occurred:\n\t* system disk encryption for \"EPHEMERAL\" is configured in both v1alpha1.Config and VolumeConfig\n\n", + }, + } { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctr, err := container.New(tt.documents...) + require.NoError(t, err) + + warnings, err := ctr.Validate(validationMode{}) + + if tt.expectedError == "" { + require.NoError(t, err) + } else { + require.EqualError(t, err, tt.expectedError) + } + + require.Equal(t, tt.expectedWarnings, warnings) + }) + } +} + +func TestValidateContainer(t *testing.T) { + t.Parallel() + + sideroLinkCfg := siderolink.NewConfigV1Alpha1() + sideroLinkCfg.APIUrlConfig.URL = must.Value(url.Parse("https://siderolink.api/?jointoken=secret&user=alice"))(t) + + v1alpha1Cfg := &v1alpha1.Config{ + ClusterConfig: &v1alpha1.ClusterConfig{ + ControlPlane: &v1alpha1.ControlPlaneConfig{ + Endpoint: &v1alpha1.Endpoint{ + URL: must.Value(url.Parse("https://localhost:6443"))(t), + }, + }, + }, + MachineConfig: &v1alpha1.MachineConfig{ + MachineType: "worker", + MachineCA: &x509.PEMEncodedCertificateAndKey{ + Crt: []byte("cert"), + }, + }, + } + + v1alpha1CfgHostDNS := v1alpha1Cfg.DeepCopy() + v1alpha1CfgHostDNS.MachineConfig.MachineFeatures = &v1alpha1.FeaturesConfig{ + HostDNSSupport: &v1alpha1.HostDNSConfig{ //nolint:staticcheck // testing legacy features + HostDNSConfigEnabled: new(true), + HostDNSForwardKubeDNSToHost: new(true), + }, + } + + resolverConfig := network.NewResolverConfigV1Alpha1() + resolverConfig.ResolverNameservers = []network.NameserverConfig{ + { + Address: network.Addr{Addr: netip.MustParseAddr("1.1.1.1")}, + }, + } + + hostDNSResolverConfig := network.NewResolverConfigV1Alpha1() + hostDNSResolverConfig.ResolverHostDNS = network.HostDNSConfig{ + HostDNSEnabled: new(true), + HostDNSForwardKubeDNSToHost: new(true), + } + + for _, tt := range []struct { + name string + documents []config.Document + inContainer bool + + expectedError string + }{ + { + name: "empty !container", + }, + { + name: "empty container", + inContainer: true, + + expectedError: "1 error occurred:\n\t* hostDNS config is required in container mode\n\n", + }, + { + name: "empty v1alpha1 container", + documents: []config.Document{v1alpha1Cfg}, + inContainer: true, + + expectedError: "1 error occurred:\n\t* hostDNS config is required in container mode\n\n", + }, + { + name: "just resolver in container", + documents: []config.Document{resolverConfig}, + inContainer: true, + + expectedError: "1 error occurred:\n\t* hostDNS config is required in container mode\n\n", + }, + { + name: "hostDNS v1alpha1 container", + documents: []config.Document{v1alpha1CfgHostDNS}, + inContainer: true, + }, + { + name: "hostDNS v1alpha1 container plus multi-doc", + documents: []config.Document{v1alpha1CfgHostDNS, resolverConfig}, + inContainer: true, + }, + { + name: "just multi-doc with hostDNS", + documents: []config.Document{hostDNSResolverConfig}, + inContainer: true, + }, + { + name: "multi-doc with hostDNS and v1alpha1", + documents: []config.Document{hostDNSResolverConfig, v1alpha1Cfg}, + inContainer: true, + }, + } { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctr, err := container.New(tt.documents...) + require.NoError(t, err) + + warnings, err := ctr.Validate(validationMode{inContainer: tt.inContainer}) + + if tt.expectedError == "" { + require.NoError(t, err) + } else { + require.EqualError(t, err, tt.expectedError) + } + + require.Nil(t, warnings) + }) + } +} + +type validationMode struct { + inContainer bool +} + +func (validationMode) String() string { + return "" +} + +func (validationMode) RequiresInstall() bool { + return false +} + +func (v validationMode) InContainer() bool { + return v.inContainer +} diff --git a/pkg/machinery/config/contract.go b/pkg/machinery/config/contract.go index 479c8d940..60363b6d4 100644 --- a/pkg/machinery/config/contract.go +++ b/pkg/machinery/config/contract.go @@ -209,3 +209,8 @@ func (contract *VersionContract) GrubUseUKICmdlineDefault() bool { func (contract *VersionContract) KubeSpanMultidocConfig() bool { return contract.Greater(TalosVersion1_12) } + +// HostDNSMultidocConfig returns true if version of Talos should use HostDNS multi-doc config. +func (contract *VersionContract) HostDNSMultidocConfig() bool { + return contract.Greater(TalosVersion1_13) +} diff --git a/pkg/machinery/config/contract_test.go b/pkg/machinery/config/contract_test.go index 3f7969844..f930b0d6b 100644 --- a/pkg/machinery/config/contract_test.go +++ b/pkg/machinery/config/contract_test.go @@ -72,6 +72,7 @@ func TestContractCurrent(t *testing.T) { assert.False(t, contract.PopulateClusterSANsFromEndpoint()) assert.True(t, contract.GrubUseUKICmdlineDefault()) assert.True(t, contract.KubeSpanMultidocConfig()) + assert.True(t, contract.HostDNSMultidocConfig()) } func TestContract1_14(t *testing.T) { @@ -102,6 +103,7 @@ func TestContract1_14(t *testing.T) { assert.False(t, contract.PopulateClusterSANsFromEndpoint()) assert.True(t, contract.GrubUseUKICmdlineDefault()) assert.True(t, contract.KubeSpanMultidocConfig()) + assert.True(t, contract.HostDNSMultidocConfig()) } func TestContract1_13(t *testing.T) { @@ -132,6 +134,7 @@ func TestContract1_13(t *testing.T) { assert.False(t, contract.PopulateClusterSANsFromEndpoint()) assert.True(t, contract.GrubUseUKICmdlineDefault()) assert.True(t, contract.KubeSpanMultidocConfig()) + assert.False(t, contract.HostDNSMultidocConfig()) } func TestContract1_12(t *testing.T) { @@ -162,6 +165,7 @@ func TestContract1_12(t *testing.T) { assert.False(t, contract.PopulateClusterSANsFromEndpoint()) assert.True(t, contract.GrubUseUKICmdlineDefault()) assert.False(t, contract.KubeSpanMultidocConfig()) + assert.False(t, contract.HostDNSMultidocConfig()) } func TestContract1_11(t *testing.T) { @@ -192,6 +196,7 @@ func TestContract1_11(t *testing.T) { assert.True(t, contract.PopulateClusterSANsFromEndpoint()) assert.False(t, contract.GrubUseUKICmdlineDefault()) assert.False(t, contract.KubeSpanMultidocConfig()) + assert.False(t, contract.HostDNSMultidocConfig()) } func TestContract1_10(t *testing.T) { @@ -222,6 +227,7 @@ func TestContract1_10(t *testing.T) { assert.True(t, contract.PopulateClusterSANsFromEndpoint()) assert.False(t, contract.GrubUseUKICmdlineDefault()) assert.False(t, contract.KubeSpanMultidocConfig()) + assert.False(t, contract.HostDNSMultidocConfig()) } func TestContract1_9(t *testing.T) { @@ -252,6 +258,7 @@ func TestContract1_9(t *testing.T) { assert.True(t, contract.PopulateClusterSANsFromEndpoint()) assert.False(t, contract.GrubUseUKICmdlineDefault()) assert.False(t, contract.KubeSpanMultidocConfig()) + assert.False(t, contract.HostDNSMultidocConfig()) } func TestContract1_8(t *testing.T) { @@ -282,6 +289,7 @@ func TestContract1_8(t *testing.T) { assert.True(t, contract.PopulateClusterSANsFromEndpoint()) assert.False(t, contract.GrubUseUKICmdlineDefault()) assert.False(t, contract.KubeSpanMultidocConfig()) + assert.False(t, contract.HostDNSMultidocConfig()) } func TestContract1_7(t *testing.T) { @@ -312,6 +320,7 @@ func TestContract1_7(t *testing.T) { assert.True(t, contract.PopulateClusterSANsFromEndpoint()) assert.False(t, contract.GrubUseUKICmdlineDefault()) assert.False(t, contract.KubeSpanMultidocConfig()) + assert.False(t, contract.HostDNSMultidocConfig()) } func TestContract1_6(t *testing.T) { @@ -342,6 +351,7 @@ func TestContract1_6(t *testing.T) { assert.True(t, contract.PopulateClusterSANsFromEndpoint()) assert.False(t, contract.GrubUseUKICmdlineDefault()) assert.False(t, contract.KubeSpanMultidocConfig()) + assert.False(t, contract.HostDNSMultidocConfig()) } func TestContract1_5(t *testing.T) { @@ -372,6 +382,7 @@ func TestContract1_5(t *testing.T) { assert.True(t, contract.PopulateClusterSANsFromEndpoint()) assert.False(t, contract.GrubUseUKICmdlineDefault()) assert.False(t, contract.KubeSpanMultidocConfig()) + assert.False(t, contract.HostDNSMultidocConfig()) } func TestContract1_4(t *testing.T) { @@ -402,6 +413,7 @@ func TestContract1_4(t *testing.T) { assert.True(t, contract.PopulateClusterSANsFromEndpoint()) assert.False(t, contract.GrubUseUKICmdlineDefault()) assert.False(t, contract.KubeSpanMultidocConfig()) + assert.False(t, contract.HostDNSMultidocConfig()) } func TestContract1_3(t *testing.T) { @@ -432,6 +444,7 @@ func TestContract1_3(t *testing.T) { assert.True(t, contract.PopulateClusterSANsFromEndpoint()) assert.False(t, contract.GrubUseUKICmdlineDefault()) assert.False(t, contract.KubeSpanMultidocConfig()) + assert.False(t, contract.HostDNSMultidocConfig()) } func TestContract1_2(t *testing.T) { @@ -462,6 +475,7 @@ func TestContract1_2(t *testing.T) { assert.True(t, contract.PopulateClusterSANsFromEndpoint()) assert.False(t, contract.GrubUseUKICmdlineDefault()) assert.False(t, contract.KubeSpanMultidocConfig()) + assert.False(t, contract.HostDNSMultidocConfig()) } func TestContract1_1(t *testing.T) { @@ -492,6 +506,7 @@ func TestContract1_1(t *testing.T) { assert.True(t, contract.PopulateClusterSANsFromEndpoint()) assert.False(t, contract.GrubUseUKICmdlineDefault()) assert.False(t, contract.KubeSpanMultidocConfig()) + assert.False(t, contract.HostDNSMultidocConfig()) } func TestContract1_0(t *testing.T) { @@ -522,4 +537,5 @@ func TestContract1_0(t *testing.T) { assert.True(t, contract.PopulateClusterSANsFromEndpoint()) assert.False(t, contract.GrubUseUKICmdlineDefault()) assert.False(t, contract.KubeSpanMultidocConfig()) + assert.False(t, contract.HostDNSMultidocConfig()) } diff --git a/pkg/machinery/config/generate/generate_test.go b/pkg/machinery/config/generate/generate_test.go index 14d4d4a86..ed59e74cb 100644 --- a/pkg/machinery/config/generate/generate_test.go +++ b/pkg/machinery/config/generate/generate_test.go @@ -9,6 +9,7 @@ import ( "fmt" "testing" + "github.com/siderolabs/gen/xslices" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -18,6 +19,7 @@ import ( mc "github.com/siderolabs/talos/pkg/machinery/config/config" "github.com/siderolabs/talos/pkg/machinery/config/generate" "github.com/siderolabs/talos/pkg/machinery/config/machine" + "github.com/siderolabs/talos/pkg/machinery/config/types/cri" "github.com/siderolabs/talos/pkg/machinery/constants" "github.com/siderolabs/talos/pkg/machinery/role" ) @@ -148,14 +150,18 @@ func TestGenerateRegistryMirrorsOrder(t *testing.T) { require.NoError(t, err) cfg, err := input.Config(machine.TypeControlPlane) - require.NoError(t, err) - named, ok := cfg.Documents()[1].(mc.NamedDocument) + registryConfigs := xslices.Filter(cfg.Documents(), func(doc mc.Document) bool { + return doc.Kind() == cri.RegistryMirrorConfig + }) + require.Len(t, registryConfigs, 2) + + named, ok := registryConfigs[0].(mc.NamedDocument) require.True(t, ok) assert.Equal(t, "a.com", named.Name()) - named, ok = cfg.Documents()[2].(mc.NamedDocument) + named, ok = registryConfigs[1].(mc.NamedDocument) require.True(t, ok) assert.Equal(t, "b.com", named.Name()) } diff --git a/pkg/machinery/config/generate/init.go b/pkg/machinery/config/generate/init.go index 7a8c40a0f..7e719f43f 100644 --- a/pkg/machinery/config/generate/init.go +++ b/pkg/machinery/config/generate/init.go @@ -10,6 +10,7 @@ import ( "github.com/siderolabs/talos/pkg/machinery/config/config" "github.com/siderolabs/talos/pkg/machinery/config/machine" + "github.com/siderolabs/talos/pkg/machinery/config/types/network" v1alpha1 "github.com/siderolabs/talos/pkg/machinery/config/types/v1alpha1" "github.com/siderolabs/talos/pkg/machinery/constants" ) @@ -79,9 +80,9 @@ func (in *Input) init() ([]config.Document, error) { machine.MachineKubelet.KubeletDisableManifestsDirectory = new(true) } - if in.Options.VersionContract.HostDNSEnabled() { - machine.MachineFeatures.HostDNSSupport = &v1alpha1.HostDNSConfig{ - HostDNSEnabled: new(true), + if in.Options.VersionContract.HostDNSEnabled() && !in.Options.VersionContract.HostDNSMultidocConfig() { + machine.MachineFeatures.HostDNSSupport = &v1alpha1.HostDNSConfig{ //nolint:staticcheck // legacy configuration + HostDNSConfigEnabled: new(true), HostDNSForwardKubeDNSToHost: ptrOrNil(in.Options.HostDNSForwardKubeDNSToHost.ValueOrZero() || in.Options.VersionContract.HostDNSForwardKubeDNSToHost()), } } @@ -210,6 +211,16 @@ func (in *Input) init() ([]config.Document, error) { documents := []config.Document{v1alpha1Config} + if in.Options.VersionContract.HostDNSEnabled() && in.Options.VersionContract.HostDNSMultidocConfig() { + resolverConfig := network.NewResolverConfigV1Alpha1() + resolverConfig.ResolverHostDNS = network.HostDNSConfig{ + HostDNSEnabled: new(true), + HostDNSForwardKubeDNSToHost: ptrOrNil(in.Options.HostDNSForwardKubeDNSToHost.ValueOrZero() || in.Options.VersionContract.HostDNSForwardKubeDNSToHost()), + } + + documents = append(documents, resolverConfig) + } + extraDocuments, err := in.generateRegistryConfigs(machine) if err != nil { return nil, fmt.Errorf("failed to generate registry configs: %w", err) diff --git a/pkg/machinery/config/generate/worker.go b/pkg/machinery/config/generate/worker.go index c88d7d8bc..83fd208cd 100644 --- a/pkg/machinery/config/generate/worker.go +++ b/pkg/machinery/config/generate/worker.go @@ -12,6 +12,7 @@ import ( "github.com/siderolabs/talos/pkg/machinery/config/config" "github.com/siderolabs/talos/pkg/machinery/config/machine" + "github.com/siderolabs/talos/pkg/machinery/config/types/network" v1alpha1 "github.com/siderolabs/talos/pkg/machinery/config/types/v1alpha1" "github.com/siderolabs/talos/pkg/machinery/constants" ) @@ -81,9 +82,9 @@ func (in *Input) worker() ([]config.Document, error) { machine.MachineKubelet.KubeletDisableManifestsDirectory = new(true) } - if in.Options.VersionContract.HostDNSEnabled() { - machine.MachineFeatures.HostDNSSupport = &v1alpha1.HostDNSConfig{ - HostDNSEnabled: new(true), + if in.Options.VersionContract.HostDNSEnabled() && !in.Options.VersionContract.HostDNSMultidocConfig() { + machine.MachineFeatures.HostDNSSupport = &v1alpha1.HostDNSConfig{ //nolint:staticcheck // legacy config + HostDNSConfigEnabled: new(true), HostDNSForwardKubeDNSToHost: ptrOrNil(in.Options.HostDNSForwardKubeDNSToHost.ValueOrZero() || in.Options.VersionContract.HostDNSForwardKubeDNSToHost()), } } @@ -143,6 +144,16 @@ func (in *Input) worker() ([]config.Document, error) { documents := []config.Document{v1alpha1Config} + if in.Options.VersionContract.HostDNSEnabled() && in.Options.VersionContract.HostDNSMultidocConfig() { + resolverConfig := network.NewResolverConfigV1Alpha1() + resolverConfig.ResolverHostDNS = network.HostDNSConfig{ + HostDNSEnabled: new(true), + HostDNSForwardKubeDNSToHost: ptrOrNil(in.Options.HostDNSForwardKubeDNSToHost.ValueOrZero() || in.Options.VersionContract.HostDNSForwardKubeDNSToHost()), + } + + documents = append(documents, resolverConfig) + } + extraDocuments, err := in.generateRegistryConfigs(machine) if err != nil { return nil, fmt.Errorf("failed to generate registry configs: %w", err) diff --git a/pkg/machinery/config/schemas/config.schema.json b/pkg/machinery/config/schemas/config.schema.json index 355c6380a..cfcf26649 100644 --- a/pkg/machinery/config/schemas/config.schema.json +++ b/pkg/machinery/config/schemas/config.schema.json @@ -2226,6 +2226,34 @@ ], "description": "HCloudVIPConfig is a config document to configure virtual IP using Hetzner Cloud APIs for announcement.\\nVirtual IP configuration should be used only on controlplane nodes to provide virtual IP for Kubernetes API server.\\nAny other use cases are not supported and may lead to unexpected behavior.\\nVirtual IP will be announced from only one node at a time using Hetzner Cloud APIs.\\n" }, + "network.HostDNSConfig": { + "properties": { + "enabled": { + "type": "boolean", + "title": "enabled", + "description": "Enable host DNS caching resolver.\n\nWhen enabled, a local DNS caching resolver is deployed on the host to improve DNS resolution performance and reliability.\nUpstream DNS servers for the host resolver are configured using the nameservers field in this config document.\n", + "markdownDescription": "Enable host DNS caching resolver.\n\nWhen enabled, a local DNS caching resolver is deployed on the host to improve DNS resolution performance and reliability.\nUpstream DNS servers for the host resolver are configured using the `nameservers` field in this config document.", + "x-intellij-html-description": "\u003cp\u003eEnable host DNS caching resolver.\u003c/p\u003e\n\n\u003cp\u003eWhen enabled, a local DNS caching resolver is deployed on the host to improve DNS resolution performance and reliability.\nUpstream DNS servers for the host resolver are configured using the \u003ccode\u003enameservers\u003c/code\u003e field in this config document.\u003c/p\u003e\n" + }, + "forwardKubeDNSToHost": { + "type": "boolean", + "title": "forwardKubeDNSToHost", + "description": "Use the host DNS resolver as upstream for Kubernetes CoreDNS pods.\n\nWhen enabled, CoreDNS pods use host DNS server as the upstream DNS (instead of\nusing configured upstream DNS resolvers directly).\n", + "markdownDescription": "Use the host DNS resolver as upstream for Kubernetes CoreDNS pods.\n\nWhen enabled, CoreDNS pods use host DNS server as the upstream DNS (instead of\nusing configured upstream DNS resolvers directly).", + "x-intellij-html-description": "\u003cp\u003eUse the host DNS resolver as upstream for Kubernetes CoreDNS pods.\u003c/p\u003e\n\n\u003cp\u003eWhen enabled, CoreDNS pods use host DNS server as the upstream DNS (instead of\nusing configured upstream DNS resolvers directly).\u003c/p\u003e\n" + }, + "resolveMemberNames": { + "type": "boolean", + "title": "resolveMemberNames", + "description": "Resolve member hostnames using the host DNS resolver.\n\nWhen enabled, cluster member hostnames and node names are resolved using the host DNS resolver.\nThis requires service discovery to be enabled.\n", + "markdownDescription": "Resolve member hostnames using the host DNS resolver.\n\nWhen enabled, cluster member hostnames and node names are resolved using the host DNS resolver.\nThis requires service discovery to be enabled.", + "x-intellij-html-description": "\u003cp\u003eResolve member hostnames using the host DNS resolver.\u003c/p\u003e\n\n\u003cp\u003eWhen enabled, cluster member hostnames and node names are resolved using the host DNS resolver.\nThis requires service discovery to be enabled.\u003c/p\u003e\n" + } + }, + "additionalProperties": false, + "type": "object", + "description": "HostDNSConfig represents host DNS configuration." + }, "network.HostnameConfigV1Alpha1": { "properties": { "apiVersion": { @@ -2705,6 +2733,13 @@ "description": "Configuration for search domains (in /etc/resolv.conf).\n\nThe default is to derive search domains from the hostname FQDN.\n", "markdownDescription": "Configuration for search domains (in /etc/resolv.conf).\n\nThe default is to derive search domains from the hostname FQDN.", "x-intellij-html-description": "\u003cp\u003eConfiguration for search domains (in /etc/resolv.conf).\u003c/p\u003e\n\n\u003cp\u003eThe default is to derive search domains from the hostname FQDN.\u003c/p\u003e\n" + }, + "hostDNS": { + "$ref": "#/$defs/network.HostDNSConfig", + "title": "hostDNS", + "description": "Configuration for host DNS resolver.\n\nThis configures a local DNS caching resolver on the host to improve DNS resolution performance and reliability.\n", + "markdownDescription": "Configuration for host DNS resolver.\n\nThis configures a local DNS caching resolver on the host to improve DNS resolution performance and reliability.", + "x-intellij-html-description": "\u003cp\u003eConfiguration for host DNS resolver.\u003c/p\u003e\n\n\u003cp\u003eThis configures a local DNS caching resolver on the host to improve DNS resolution performance and reliability.\u003c/p\u003e\n" } }, "additionalProperties": false, @@ -4674,13 +4709,6 @@ "markdownDescription": "KubePrism - local proxy/load balancer on defined port that will distribute\nrequests to all API servers in the cluster.", "x-intellij-html-description": "\u003cp\u003eKubePrism - local proxy/load balancer on defined port that will distribute\nrequests to all API servers in the cluster.\u003c/p\u003e\n" }, - "hostDNS": { - "$ref": "#/$defs/v1alpha1.HostDNSConfig", - "title": "hostDNS", - "description": "Configures host DNS caching resolver.\n", - "markdownDescription": "Configures host DNS caching resolver.", - "x-intellij-html-description": "\u003cp\u003eConfigures host DNS caching resolver.\u003c/p\u003e\n" - }, "imageCache": { "$ref": "#/$defs/v1alpha1.ImageCacheConfig", "title": "imageCache", @@ -4724,34 +4752,6 @@ "type": "object", "description": "FlannelCNIConfig represents the Flannel CNI configuration options." }, - "v1alpha1.HostDNSConfig": { - "properties": { - "enabled": { - "type": "boolean", - "title": "enabled", - "description": "Enable host DNS caching resolver.\n", - "markdownDescription": "Enable host DNS caching resolver.", - "x-intellij-html-description": "\u003cp\u003eEnable host DNS caching resolver.\u003c/p\u003e\n" - }, - "forwardKubeDNSToHost": { - "type": "boolean", - "title": "forwardKubeDNSToHost", - "description": "Use the host DNS resolver as upstream for Kubernetes CoreDNS pods.\n\nWhen enabled, CoreDNS pods use host DNS server as the upstream DNS (instead of\nusing configured upstream DNS resolvers directly).\n", - "markdownDescription": "Use the host DNS resolver as upstream for Kubernetes CoreDNS pods.\n\nWhen enabled, CoreDNS pods use host DNS server as the upstream DNS (instead of\nusing configured upstream DNS resolvers directly).", - "x-intellij-html-description": "\u003cp\u003eUse the host DNS resolver as upstream for Kubernetes CoreDNS pods.\u003c/p\u003e\n\n\u003cp\u003eWhen enabled, CoreDNS pods use host DNS server as the upstream DNS (instead of\nusing configured upstream DNS resolvers directly).\u003c/p\u003e\n" - }, - "resolveMemberNames": { - "type": "boolean", - "title": "resolveMemberNames", - "description": "Resolve member hostnames using the host DNS resolver.\n\nWhen enabled, cluster member hostnames and node names are resolved using the host DNS resolver.\nThis requires service discovery to be enabled.\n", - "markdownDescription": "Resolve member hostnames using the host DNS resolver.\n\nWhen enabled, cluster member hostnames and node names are resolved using the host DNS resolver.\nThis requires service discovery to be enabled.", - "x-intellij-html-description": "\u003cp\u003eResolve member hostnames using the host DNS resolver.\u003c/p\u003e\n\n\u003cp\u003eWhen enabled, cluster member hostnames and node names are resolved using the host DNS resolver.\nThis requires service discovery to be enabled.\u003c/p\u003e\n" - } - }, - "additionalProperties": false, - "type": "object", - "description": "HostDNSConfig describes the configuration for the host DNS resolver." - }, "v1alpha1.ImageCacheConfig": { "properties": { "localEnabled": { diff --git a/pkg/machinery/config/types/network/deep_copy.generated.go b/pkg/machinery/config/types/network/deep_copy.generated.go index 25f4a38aa..921c6b5da 100644 --- a/pkg/machinery/config/types/network/deep_copy.generated.go +++ b/pkg/machinery/config/types/network/deep_copy.generated.go @@ -515,6 +515,18 @@ func (o *ResolverConfigV1Alpha1) DeepCopy() *ResolverConfigV1Alpha1 { cp.ResolverSearchDomains.SearchDisableDefault = new(bool) *cp.ResolverSearchDomains.SearchDisableDefault = *o.ResolverSearchDomains.SearchDisableDefault } + if o.ResolverHostDNS.HostDNSEnabled != nil { + cp.ResolverHostDNS.HostDNSEnabled = new(bool) + *cp.ResolverHostDNS.HostDNSEnabled = *o.ResolverHostDNS.HostDNSEnabled + } + if o.ResolverHostDNS.HostDNSForwardKubeDNSToHost != nil { + cp.ResolverHostDNS.HostDNSForwardKubeDNSToHost = new(bool) + *cp.ResolverHostDNS.HostDNSForwardKubeDNSToHost = *o.ResolverHostDNS.HostDNSForwardKubeDNSToHost + } + if o.ResolverHostDNS.HostDNSResolveMemberNames != nil { + cp.ResolverHostDNS.HostDNSResolveMemberNames = new(bool) + *cp.ResolverHostDNS.HostDNSResolveMemberNames = *o.ResolverHostDNS.HostDNSResolveMemberNames + } return &cp } diff --git a/pkg/machinery/config/types/network/network_doc.go b/pkg/machinery/config/types/network/network_doc.go index 2fec5c37c..215054c1c 100644 --- a/pkg/machinery/config/types/network/network_doc.go +++ b/pkg/machinery/config/types/network/network_doc.go @@ -1450,6 +1450,13 @@ func (ResolverConfigV1Alpha1) Doc() *encoder.Doc { Description: "Configuration for search domains (in /etc/resolv.conf).\n\nThe default is to derive search domains from the hostname FQDN.", Comments: [3]string{"" /* encoder.HeadComment */, "Configuration for search domains (in /etc/resolv.conf)." /* encoder.LineComment */, "" /* encoder.FootComment */}, }, + { + Name: "hostDNS", + Type: "HostDNSConfig", + Note: "", + Description: "Configuration for host DNS resolver.\n\nThis configures a local DNS caching resolver on the host to improve DNS resolution performance and reliability.", + Comments: [3]string{"" /* encoder.HeadComment */, "Configuration for host DNS resolver." /* encoder.LineComment */, "" /* encoder.FootComment */}, + }, }, } @@ -1457,6 +1464,8 @@ func (ResolverConfigV1Alpha1) Doc() *encoder.Doc { doc.AddExample("", exampleResolverConfigV1Alpha2()) + doc.AddExample("", exampleResolverConfigV1Alpha3()) + return doc } @@ -1519,6 +1528,45 @@ func (SearchDomainsConfig) Doc() *encoder.Doc { return doc } +func (HostDNSConfig) Doc() *encoder.Doc { + doc := &encoder.Doc{ + Type: "HostDNSConfig", + Comments: [3]string{"" /* encoder.HeadComment */, "HostDNSConfig represents host DNS configuration." /* encoder.LineComment */, "" /* encoder.FootComment */}, + Description: "HostDNSConfig represents host DNS configuration.", + AppearsIn: []encoder.Appearance{ + { + TypeName: "ResolverConfigV1Alpha1", + FieldName: "hostDNS", + }, + }, + Fields: []encoder.Doc{ + { + Name: "enabled", + Type: "bool", + Note: "", + Description: "Enable host DNS caching resolver.\n\nWhen enabled, a local DNS caching resolver is deployed on the host to improve DNS resolution performance and reliability.\nUpstream DNS servers for the host resolver are configured using the `nameservers` field in this config document.", + Comments: [3]string{"" /* encoder.HeadComment */, "Enable host DNS caching resolver." /* encoder.LineComment */, "" /* encoder.FootComment */}, + }, + { + Name: "forwardKubeDNSToHost", + Type: "bool", + Note: "", + Description: "Use the host DNS resolver as upstream for Kubernetes CoreDNS pods.\n\nWhen enabled, CoreDNS pods use host DNS server as the upstream DNS (instead of\nusing configured upstream DNS resolvers directly).", + Comments: [3]string{"" /* encoder.HeadComment */, "Use the host DNS resolver as upstream for Kubernetes CoreDNS pods." /* encoder.LineComment */, "" /* encoder.FootComment */}, + }, + { + Name: "resolveMemberNames", + Type: "bool", + Note: "", + Description: "Resolve member hostnames using the host DNS resolver.\n\nWhen enabled, cluster member hostnames and node names are resolved using the host DNS resolver.\nThis requires service discovery to be enabled.", + Comments: [3]string{"" /* encoder.HeadComment */, "Resolve member hostnames using the host DNS resolver." /* encoder.LineComment */, "" /* encoder.FootComment */}, + }, + }, + } + + return doc +} + func (RoutingRuleConfigV1Alpha1) Doc() *encoder.Doc { doc := &encoder.Doc{ Type: "RoutingRuleConfig", @@ -2155,6 +2203,7 @@ func GetFileDoc() *encoder.FileDoc { ResolverConfigV1Alpha1{}.Doc(), NameserverConfig{}.Doc(), SearchDomainsConfig{}.Doc(), + HostDNSConfig{}.Doc(), RoutingRuleConfigV1Alpha1{}.Doc(), RuleConfigV1Alpha1{}.Doc(), RulePortSelector{}.Doc(), diff --git a/pkg/machinery/config/types/network/resolver.go b/pkg/machinery/config/types/network/resolver.go index 2f94c5fd8..ae83e6b38 100644 --- a/pkg/machinery/config/types/network/resolver.go +++ b/pkg/machinery/config/types/network/resolver.go @@ -11,6 +11,7 @@ import ( "net/netip" "slices" + "github.com/siderolabs/gen/value" "github.com/siderolabs/gen/xslices" "github.com/siderolabs/go-pointer" @@ -19,6 +20,7 @@ import ( "github.com/siderolabs/talos/pkg/machinery/config/internal/registry" "github.com/siderolabs/talos/pkg/machinery/config/types/meta" "github.com/siderolabs/talos/pkg/machinery/config/types/v1alpha1" + "github.com/siderolabs/talos/pkg/machinery/config/validation" ) // ResolverKind is a ResolverConfig document kind. @@ -38,6 +40,8 @@ func init() { // Check interfaces. var ( _ config.NetworkResolverConfig = &ResolverConfigV1Alpha1{} + _ config.NetworkHostDNSConfig = &ResolverConfigV1Alpha1{} + _ config.Validator = &ResolverConfigV1Alpha1{} _ container.V1Alpha1ConflictValidator = &ResolverConfigV1Alpha1{} ) @@ -46,6 +50,7 @@ var ( // examples: // - value: exampleResolverConfigV1Alpha1() // - value: exampleResolverConfigV1Alpha2() +// - value: exampleResolverConfigV1Alpha3() // alias: ResolverConfig // schemaRoot: true // schemaMeta: v1alpha1/ResolverConfig @@ -66,6 +71,11 @@ type ResolverConfigV1Alpha1 struct { // // The default is to derive search domains from the hostname FQDN. ResolverSearchDomains SearchDomainsConfig `yaml:"searchDomains,omitempty"` + // description: | + // Configuration for host DNS resolver. + // + // This configures a local DNS caching resolver on the host to improve DNS resolution performance and reliability. + ResolverHostDNS HostDNSConfig `yaml:"hostDNS,omitempty"` } // NameserverConfig represents a single nameserver configuration. @@ -101,6 +111,28 @@ type SearchDomainsConfig struct { SearchDisableDefault *bool `yaml:"disableDefault,omitempty"` } +// HostDNSConfig represents host DNS configuration. +type HostDNSConfig struct { + // description: | + // Enable host DNS caching resolver. + // + // When enabled, a local DNS caching resolver is deployed on the host to improve DNS resolution performance and reliability. + // Upstream DNS servers for the host resolver are configured using the `nameservers` field in this config document. + HostDNSEnabled *bool `yaml:"enabled,omitempty"` + // description: | + // Use the host DNS resolver as upstream for Kubernetes CoreDNS pods. + // + // When enabled, CoreDNS pods use host DNS server as the upstream DNS (instead of + // using configured upstream DNS resolvers directly). + HostDNSForwardKubeDNSToHost *bool `yaml:"forwardKubeDNSToHost,omitempty"` + // description: | + // Resolve member hostnames using the host DNS resolver. + // + // When enabled, cluster member hostnames and node names are resolved using the host DNS resolver. + // This requires service discovery to be enabled. + HostDNSResolveMemberNames *bool `yaml:"resolveMemberNames,omitempty"` +} + // NewResolverConfigV1Alpha1 creates a new ResolverConfig config document. func NewResolverConfigV1Alpha1() *ResolverConfigV1Alpha1 { return &ResolverConfigV1Alpha1{ @@ -137,6 +169,17 @@ func exampleResolverConfigV1Alpha2() *ResolverConfigV1Alpha1 { return cfg } +func exampleResolverConfigV1Alpha3() *ResolverConfigV1Alpha1 { + cfg := NewResolverConfigV1Alpha1() + cfg.ResolverHostDNS = HostDNSConfig{ + HostDNSEnabled: new(true), + HostDNSForwardKubeDNSToHost: new(true), + HostDNSResolveMemberNames: new(true), + } + + return cfg +} + // Clone implements config.Document interface. func (s *ResolverConfigV1Alpha1) Clone() config.Document { return s.DeepCopy() @@ -156,9 +199,34 @@ func (s *ResolverConfigV1Alpha1) V1Alpha1ConflictValidate(v1alpha1Cfg *v1alpha1. return errors.New(".machine.network.disableSearchDomain is already set in v1alpha1 config") } + if !value.IsZero(s.ResolverHostDNS) { + if v1alpha1Cfg.NetworkHostDNSConfig() != nil { + return errors.New(".machine.features.hostDNS is already set in v1alpha1 config") + } + } + return nil } +// Validate implements config.Validator interface. +func (s *ResolverConfigV1Alpha1) Validate(validation.RuntimeMode, ...validation.Option) ([]string, error) { + var errs error + + if !value.IsZero(s.ResolverHostDNS) { + if !s.HostDNSEnabled() { + if s.ForwardKubeDNSToHost() { + errs = errors.Join(errs, errors.New("hostDNS.forwardKubeDNSToHost cannot be enabled when hostDNS.enabled is false")) + } + + if s.ResolveMemberNames() { + errs = errors.Join(errs, errors.New("hostDNS.resolveMemberNames cannot be enabled when hostDNS.enabled is false")) + } + } + } + + return nil, errs +} + // Resolvers implements NetworkResolverConfig interface. func (s *ResolverConfigV1Alpha1) Resolvers() []netip.Addr { return xslices.Map(s.ResolverNameservers, func(ns NameserverConfig) netip.Addr { @@ -175,3 +243,18 @@ func (s *ResolverConfigV1Alpha1) SearchDomains() []string { func (s *ResolverConfigV1Alpha1) DisableSearchDomain() bool { return pointer.SafeDeref(s.ResolverSearchDomains.SearchDisableDefault) } + +// HostDNSEnabled implements NetworkHostDNSConfig interface. +func (s *ResolverConfigV1Alpha1) HostDNSEnabled() bool { + return pointer.SafeDeref(s.ResolverHostDNS.HostDNSEnabled) +} + +// ForwardKubeDNSToHost implements NetworkHostDNSConfig interface. +func (s *ResolverConfigV1Alpha1) ForwardKubeDNSToHost() bool { + return pointer.SafeDeref(s.ResolverHostDNS.HostDNSForwardKubeDNSToHost) +} + +// ResolveMemberNames implements NetworkHostDNSConfig interface. +func (s *ResolverConfigV1Alpha1) ResolveMemberNames() bool { + return pointer.SafeDeref(s.ResolverHostDNS.HostDNSResolveMemberNames) +} diff --git a/pkg/machinery/config/types/network/resolver_test.go b/pkg/machinery/config/types/network/resolver_test.go index 4ffda218b..0e8858443 100644 --- a/pkg/machinery/config/types/network/resolver_test.go +++ b/pkg/machinery/config/types/network/resolver_test.go @@ -22,6 +22,9 @@ import ( //go:embed testdata/resolverconfig.yaml var expectedResolverConfigDocument []byte +//go:embed testdata/resolverconfig_with_hostdns.yaml +var expectedResolverConfigDocumentWithHostDNS []byte + func TestResolverConfigMarshalStability(t *testing.T) { t.Parallel() @@ -47,6 +50,29 @@ func TestResolverConfigMarshalStability(t *testing.T) { assert.Equal(t, expectedResolverConfigDocument, marshaled) } +func TestResolverConfigMarshalStabilityWithHostDNS(t *testing.T) { + t.Parallel() + + cfg := network.NewResolverConfigV1Alpha1() + cfg.ResolverNameservers = []network.NameserverConfig{ + { + Address: network.Addr{Addr: netip.MustParseAddr("10.0.0.1")}, + }, + } + cfg.ResolverHostDNS = network.HostDNSConfig{ + HostDNSEnabled: new(true), + HostDNSForwardKubeDNSToHost: new(true), + HostDNSResolveMemberNames: new(false), + } + + marshaled, err := encoder.NewEncoder(cfg, encoder.WithComments(encoder.CommentsDisabled)).Encode() + require.NoError(t, err) + + t.Log(string(marshaled)) + + assert.Equal(t, expectedResolverConfigDocumentWithHostDNS, marshaled) +} + func TestResolverConfigUnmarshal(t *testing.T) { t.Parallel() @@ -76,7 +102,7 @@ func TestResolverConfigUnmarshal(t *testing.T) { }, docs[0]) } -func TestResolverV1Alpha1Validate(t *testing.T) { +func TestResolverV1Alpha1ConflictValidate(t *testing.T) { t.Parallel() for _, test := range []struct { @@ -130,6 +156,57 @@ func TestResolverV1Alpha1Validate(t *testing.T) { expectedError: ".machine.network.disableSearchDomain is already set in v1alpha1 config", }, + { + name: "v1alpha1 hostDNS and resolver hostDNS set", + v1alpha1Cfg: &v1alpha1.Config{ + MachineConfig: &v1alpha1.MachineConfig{ + MachineFeatures: &v1alpha1.FeaturesConfig{ + HostDNSSupport: &v1alpha1.HostDNSConfig{ //nolint:staticcheck // testing legacy features + HostDNSConfigEnabled: new(true), + HostDNSForwardKubeDNSToHost: new(true), + }, + }, + }, + }, + cfg: func() *network.ResolverConfigV1Alpha1 { + cfg := network.NewResolverConfigV1Alpha1() + cfg.ResolverHostDNS = network.HostDNSConfig{ + HostDNSEnabled: new(true), + HostDNSForwardKubeDNSToHost: new(true), + } + + return cfg + }, + + expectedError: ".machine.features.hostDNS is already set in v1alpha1 config", + }, + { + name: "v1alpha1 hostDNS and no resolver hostDNS set", + v1alpha1Cfg: &v1alpha1.Config{ + MachineConfig: &v1alpha1.MachineConfig{ + MachineFeatures: &v1alpha1.FeaturesConfig{ + HostDNSSupport: &v1alpha1.HostDNSConfig{ //nolint:staticcheck // testing legacy features + HostDNSConfigEnabled: new(true), + HostDNSForwardKubeDNSToHost: new(true), + }, + }, + }, + }, + cfg: network.NewResolverConfigV1Alpha1, + }, + { + name: "v1alpha1 no hostDNS and resolver hostDNS set", + v1alpha1Cfg: &v1alpha1.Config{}, + cfg: func() *network.ResolverConfigV1Alpha1 { + cfg := network.NewResolverConfigV1Alpha1() + cfg.ResolverHostDNS = network.HostDNSConfig{ + HostDNSEnabled: new(true), + HostDNSForwardKubeDNSToHost: new(true), + } + + return cfg + }, + }, } { t.Run(test.name, func(t *testing.T) { t.Parallel() @@ -143,3 +220,73 @@ func TestResolverV1Alpha1Validate(t *testing.T) { }) } } + +func TestResolverV1Alpha1Validate(t *testing.T) { + t.Parallel() + + for _, test := range []struct { + name string + cfg func() *network.ResolverConfigV1Alpha1 + + expectedError string + }{ + { + name: "empty", + cfg: network.NewResolverConfigV1Alpha1, + }, + { + name: "forwardKubeDNSToHost true but HostDNSEnabled false", + cfg: func() *network.ResolverConfigV1Alpha1 { + cfg := network.NewResolverConfigV1Alpha1() + cfg.ResolverHostDNS = network.HostDNSConfig{ + HostDNSEnabled: new(false), + HostDNSForwardKubeDNSToHost: new(true), + } + + return cfg + }, + + expectedError: "hostDNS.forwardKubeDNSToHost cannot be enabled when hostDNS.enabled is false", + }, + { + name: "resolveMemberNames true but HostDNSEnabled false", + cfg: func() *network.ResolverConfigV1Alpha1 { + cfg := network.NewResolverConfigV1Alpha1() + cfg.ResolverHostDNS = network.HostDNSConfig{ + HostDNSEnabled: new(false), + HostDNSResolveMemberNames: new(true), + } + + return cfg + }, + + expectedError: "hostDNS.resolveMemberNames cannot be enabled when hostDNS.enabled is false", + }, + { + name: "hostDNS config valid", + cfg: func() *network.ResolverConfigV1Alpha1 { + cfg := network.NewResolverConfigV1Alpha1() + cfg.ResolverHostDNS = network.HostDNSConfig{ + HostDNSEnabled: new(true), + HostDNSForwardKubeDNSToHost: new(true), + HostDNSResolveMemberNames: new(false), + } + + return cfg + }, + }, + } { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + warnings, err := test.cfg().Validate(validationMode{}) + assert.Nil(t, warnings) + + if test.expectedError != "" { + assert.EqualError(t, err, test.expectedError) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/pkg/machinery/config/types/network/testdata/resolverconfig_with_hostdns.yaml b/pkg/machinery/config/types/network/testdata/resolverconfig_with_hostdns.yaml new file mode 100644 index 000000000..ab1bfac6d --- /dev/null +++ b/pkg/machinery/config/types/network/testdata/resolverconfig_with_hostdns.yaml @@ -0,0 +1,8 @@ +apiVersion: v1alpha1 +kind: ResolverConfig +nameservers: + - address: 10.0.0.1 +hostDNS: + enabled: true + forwardKubeDNSToHost: true + resolveMemberNames: false diff --git a/pkg/machinery/config/types/v1alpha1/testdata/stability/v1.14/base-controlplane.yaml b/pkg/machinery/config/types/v1alpha1/testdata/stability/v1.14/base-controlplane.yaml index 920ab2a72..34108f4f1 100644 --- a/pkg/machinery/config/types/v1alpha1/testdata/stability/v1.14/base-controlplane.yaml +++ b/pkg/machinery/config/types/v1alpha1/testdata/stability/v1.14/base-controlplane.yaml @@ -20,9 +20,6 @@ machine: kubePrism: enabled: true port: 7445 - hostDNS: - enabled: true - forwardKubeDNSToHost: true nodeLabels: node.kubernetes.io/exclude-from-external-load-balancers: "" cluster: @@ -89,5 +86,11 @@ cluster: key: LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSU03Q2VnMk1GQW5TM3ROMzV6QTc0aFZ3VElkTkthK0ZwUHlYVERCdU4wVFlvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFNmxTeTNTekRRRmdBTHNlSXR5UU1paTVaSVJkVTFGUmMzcEZ3b3g1QUE1VHdjZ0VVQ0xaNApwMTJSNGp3ZGozWXhqbmxLYW9GY3o3QVR5ME5mWTdMVWt3PT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo= --- apiVersion: v1alpha1 +kind: ResolverConfig +hostDNS: + enabled: true + forwardKubeDNSToHost: true +--- +apiVersion: v1alpha1 kind: HostnameConfig auto: stable diff --git a/pkg/machinery/config/types/v1alpha1/testdata/stability/v1.14/base-worker.yaml b/pkg/machinery/config/types/v1alpha1/testdata/stability/v1.14/base-worker.yaml index da7ddbbac..52005daf1 100644 --- a/pkg/machinery/config/types/v1alpha1/testdata/stability/v1.14/base-worker.yaml +++ b/pkg/machinery/config/types/v1alpha1/testdata/stability/v1.14/base-worker.yaml @@ -20,9 +20,6 @@ machine: kubePrism: enabled: true port: 7445 - hostDNS: - enabled: true - forwardKubeDNSToHost: true cluster: id: 0raF93qnkMvF-FZNuvyGozXNdLiT2FOWSlyBaW4PR-w= secret: pofHbABZq7VXuObsdLdy/bHmz6hlMHZ3p8+6WKrv1ic= @@ -47,5 +44,11 @@ cluster: service: {} --- apiVersion: v1alpha1 +kind: ResolverConfig +hostDNS: + enabled: true + forwardKubeDNSToHost: true +--- +apiVersion: v1alpha1 kind: HostnameConfig auto: stable diff --git a/pkg/machinery/config/types/v1alpha1/testdata/stability/v1.14/overrides-controlplane.yaml b/pkg/machinery/config/types/v1alpha1/testdata/stability/v1.14/overrides-controlplane.yaml index 61bb4598e..bba935094 100644 --- a/pkg/machinery/config/types/v1alpha1/testdata/stability/v1.14/overrides-controlplane.yaml +++ b/pkg/machinery/config/types/v1alpha1/testdata/stability/v1.14/overrides-controlplane.yaml @@ -34,9 +34,6 @@ machine: kubePrism: enabled: true port: 7445 - hostDNS: - enabled: true - forwardKubeDNSToHost: true nodeLabels: node.kubernetes.io/exclude-from-external-load-balancers: "" cluster: @@ -112,6 +109,12 @@ cluster: allowSchedulingOnControlPlanes: true --- apiVersion: v1alpha1 +kind: ResolverConfig +hostDNS: + enabled: true + forwardKubeDNSToHost: true +--- +apiVersion: v1alpha1 kind: RegistryMirrorConfig name: ghcr.io endpoints: diff --git a/pkg/machinery/config/types/v1alpha1/testdata/stability/v1.14/overrides-worker.yaml b/pkg/machinery/config/types/v1alpha1/testdata/stability/v1.14/overrides-worker.yaml index a6357acbb..1514a5fdf 100644 --- a/pkg/machinery/config/types/v1alpha1/testdata/stability/v1.14/overrides-worker.yaml +++ b/pkg/machinery/config/types/v1alpha1/testdata/stability/v1.14/overrides-worker.yaml @@ -34,9 +34,6 @@ machine: kubePrism: enabled: true port: 7445 - hostDNS: - enabled: true - forwardKubeDNSToHost: true cluster: id: 0raF93qnkMvF-FZNuvyGozXNdLiT2FOWSlyBaW4PR-w= secret: pofHbABZq7VXuObsdLdy/bHmz6hlMHZ3p8+6WKrv1ic= @@ -65,6 +62,12 @@ cluster: service: {} --- apiVersion: v1alpha1 +kind: ResolverConfig +hostDNS: + enabled: true + forwardKubeDNSToHost: true +--- +apiVersion: v1alpha1 kind: RegistryMirrorConfig name: ghcr.io endpoints: diff --git a/pkg/machinery/config/types/v1alpha1/v1alpha1_features.go b/pkg/machinery/config/types/v1alpha1/v1alpha1_features.go index 085b8874f..7f2358cfb 100644 --- a/pkg/machinery/config/types/v1alpha1/v1alpha1_features.go +++ b/pkg/machinery/config/types/v1alpha1/v1alpha1_features.go @@ -21,15 +21,6 @@ func (f *FeaturesConfig) DiskQuotaSupportEnabled() bool { return pointer.SafeDeref(f.DiskQuotaSupport) } -// HostDNS implements config.Features interface. -func (f *FeaturesConfig) HostDNS() config.HostDNS { - if f.HostDNSSupport == nil { - return &HostDNSConfig{} - } - - return f.HostDNSSupport -} - // KubePrism implements config.Features interface. func (f *FeaturesConfig) KubePrism() config.KubePrism { if f.KubePrismSupport == nil { @@ -78,17 +69,17 @@ func (a *KubePrism) Port() int { return a.ServerPort } -// Enabled implements config.HostDNS. -func (h *HostDNSConfig) Enabled() bool { - return pointer.SafeDeref(h.HostDNSEnabled) +// HostDNSEnabled implements config.NetworkHostDNSConfig interface. +func (h *HostDNSConfig) HostDNSEnabled() bool { + return pointer.SafeDeref(h.HostDNSConfigEnabled) } -// ForwardKubeDNSToHost implements config.HostDNS. +// ForwardKubeDNSToHost implements config.NetworkHostDNSConfig interface. func (h *HostDNSConfig) ForwardKubeDNSToHost() bool { return pointer.SafeDeref(h.HostDNSForwardKubeDNSToHost) } -// ResolveMemberNames implements config.HostDNS. +// ResolveMemberNames implements config.NetworkHostDNSConfig interface. func (h *HostDNSConfig) ResolveMemberNames() bool { return pointer.SafeDeref(h.HostDNSResolveMemberNames) } diff --git a/pkg/machinery/config/types/v1alpha1/v1alpha1_network_bridge.go b/pkg/machinery/config/types/v1alpha1/v1alpha1_network_bridge.go index 39d0de493..27a4c81d9 100644 --- a/pkg/machinery/config/types/v1alpha1/v1alpha1_network_bridge.go +++ b/pkg/machinery/config/types/v1alpha1/v1alpha1_network_bridge.go @@ -99,3 +99,12 @@ func (c *Config) NetworkKubeSpanConfig() config.NetworkKubeSpanConfig { return c.MachineConfig.MachineNetwork.NetworkKubeSpan } + +// NetworkHostDNSConfig implements the config.NetworkHostDNSConfig interface. +func (c *Config) NetworkHostDNSConfig() config.NetworkHostDNSConfig { + if c.MachineConfig == nil || c.MachineConfig.MachineFeatures == nil || c.MachineConfig.MachineFeatures.HostDNSSupport == nil { + return nil + } + + return c.MachineConfig.MachineFeatures.HostDNSSupport +} diff --git a/pkg/machinery/config/types/v1alpha1/v1alpha1_network_bridge_test.go b/pkg/machinery/config/types/v1alpha1/v1alpha1_network_bridge_test.go index 64b48b850..fb245d3ed 100644 --- a/pkg/machinery/config/types/v1alpha1/v1alpha1_network_bridge_test.go +++ b/pkg/machinery/config/types/v1alpha1/v1alpha1_network_bridge_test.go @@ -556,3 +556,141 @@ func TestKubeSpanBridging(t *testing.T) { }) } } + +func TestHostDNSConfigBridging(t *testing.T) { + t.Parallel() + + for _, test := range []struct { + name string + + cfg func(*testing.T) config.Config + + expectedHostDNSConfigExists bool + expectedHostDNSEnabled bool + }{ + { + name: "v1alpha1 empty", + + cfg: func(*testing.T) config.Config { + return container.NewV1Alpha1(&v1alpha1.Config{ + MachineConfig: &v1alpha1.MachineConfig{}, + }) + }, + + expectedHostDNSConfigExists: false, + }, + { + name: "v1alpha1 hostDNS enabled", + + cfg: func(*testing.T) config.Config { + return container.NewV1Alpha1(&v1alpha1.Config{ + MachineConfig: &v1alpha1.MachineConfig{ + MachineFeatures: &v1alpha1.FeaturesConfig{ + HostDNSSupport: &v1alpha1.HostDNSConfig{ //nolint:staticcheck // testing legacy features + HostDNSConfigEnabled: new(true), + }, + }, + }, + }) + }, + + expectedHostDNSConfigExists: true, + expectedHostDNSEnabled: true, + }, + { + name: "resolver config hostDNS enabled", + + cfg: func(*testing.T) config.Config { + resolverCfg := network.NewResolverConfigV1Alpha1() + resolverCfg.ResolverHostDNS = network.HostDNSConfig{ + HostDNSEnabled: new(true), + HostDNSForwardKubeDNSToHost: new(true), + HostDNSResolveMemberNames: new(true), + } + + c, err := container.New( + resolverCfg, + ) + require.NoError(t, err) + + return c + }, + + expectedHostDNSConfigExists: true, + expectedHostDNSEnabled: true, + }, + { + name: "v1alpha1 empty and resolver config hostDNS enabled", + + cfg: func(*testing.T) config.Config { + v1alpha1Cfg := &v1alpha1.Config{ + MachineConfig: &v1alpha1.MachineConfig{}, + } + + resolverCfg := network.NewResolverConfigV1Alpha1() + resolverCfg.ResolverHostDNS = network.HostDNSConfig{ + HostDNSEnabled: new(true), + HostDNSForwardKubeDNSToHost: new(true), + HostDNSResolveMemberNames: new(true), + } + + c, err := container.New( + v1alpha1Cfg, + resolverCfg, + ) + require.NoError(t, err) + + return c + }, + + expectedHostDNSConfigExists: true, + expectedHostDNSEnabled: true, + }, + { + name: "v1alpha1 hostDNS enabled and resolver empty", + + cfg: func(*testing.T) config.Config { + v1alpha1Cfg := &v1alpha1.Config{ + MachineConfig: &v1alpha1.MachineConfig{ + MachineFeatures: &v1alpha1.FeaturesConfig{ + HostDNSSupport: &v1alpha1.HostDNSConfig{ //nolint:staticcheck // testing legacy features + HostDNSConfigEnabled: new(true), + }, + }, + }, + } + + resolverCfg := network.NewResolverConfigV1Alpha1() + + c, err := container.New( + v1alpha1Cfg, + resolverCfg, + ) + require.NoError(t, err) + + return c + }, + + expectedHostDNSConfigExists: true, + expectedHostDNSEnabled: true, + }, + } { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + cfg := test.cfg(t) + + hostDNSConfig := cfg.NetworkHostDNSConfig() + + if !test.expectedHostDNSConfigExists { + require.Nil(t, hostDNSConfig) + + return + } + + require.NotNil(t, hostDNSConfig) + + assert.Equal(t, test.expectedHostDNSEnabled, hostDNSConfig.HostDNSEnabled()) + }) + } +} diff --git a/pkg/machinery/config/types/v1alpha1/v1alpha1_types.go b/pkg/machinery/config/types/v1alpha1/v1alpha1_types.go index 4aa98bba5..1503ee3ae 100644 --- a/pkg/machinery/config/types/v1alpha1/v1alpha1_types.go +++ b/pkg/machinery/config/types/v1alpha1/v1alpha1_types.go @@ -2394,8 +2394,9 @@ type FeaturesConfig struct { // KubePrism - local proxy/load balancer on defined port that will distribute // requests to all API servers in the cluster. KubePrismSupport *KubePrism `yaml:"kubePrism,omitempty"` - // description: | - // Configures host DNS caching resolver. + // docgen:nodoc + // + // Deprecated: Use ResolverConfig document instead. HostDNSSupport *HostDNSConfig `yaml:"hostDNS,omitempty"` // description: | // Enable Image Cache feature. @@ -2441,10 +2442,14 @@ type KubernetesTalosAPIAccessConfig struct { } // HostDNSConfig describes the configuration for the host DNS resolver. +// +// Deprecated: Use ResolverConfig document instead. +// +// docgen:nodoc type HostDNSConfig struct { // description: | // Enable host DNS caching resolver. - HostDNSEnabled *bool `yaml:"enabled,omitempty"` + HostDNSConfigEnabled *bool `yaml:"enabled,omitempty"` // description: | // Use the host DNS resolver as upstream for Kubernetes CoreDNS pods. // diff --git a/pkg/machinery/config/types/v1alpha1/v1alpha1_types_doc.go b/pkg/machinery/config/types/v1alpha1/v1alpha1_types_doc.go index 3369390cb..83cbd5c78 100644 --- a/pkg/machinery/config/types/v1alpha1/v1alpha1_types_doc.go +++ b/pkg/machinery/config/types/v1alpha1/v1alpha1_types_doc.go @@ -1880,13 +1880,7 @@ func (FeaturesConfig) Doc() *encoder.Doc { Description: "KubePrism - local proxy/load balancer on defined port that will distribute\nrequests to all API servers in the cluster.", Comments: [3]string{"" /* encoder.HeadComment */, "KubePrism - local proxy/load balancer on defined port that will distribute" /* encoder.LineComment */, "" /* encoder.FootComment */}, }, - { - Name: "hostDNS", - Type: "HostDNSConfig", - Note: "", - Description: "Configures host DNS caching resolver.", - Comments: [3]string{"" /* encoder.HeadComment */, "Configures host DNS caching resolver." /* encoder.LineComment */, "" /* encoder.FootComment */}, - }, + {}, { Name: "imageCache", Type: "ImageCacheConfig", @@ -2009,45 +2003,6 @@ func (KubernetesTalosAPIAccessConfig) Doc() *encoder.Doc { return doc } -func (HostDNSConfig) Doc() *encoder.Doc { - doc := &encoder.Doc{ - Type: "HostDNSConfig", - Comments: [3]string{"" /* encoder.HeadComment */, "HostDNSConfig describes the configuration for the host DNS resolver." /* encoder.LineComment */, "" /* encoder.FootComment */}, - Description: "HostDNSConfig describes the configuration for the host DNS resolver.", - AppearsIn: []encoder.Appearance{ - { - TypeName: "FeaturesConfig", - FieldName: "hostDNS", - }, - }, - Fields: []encoder.Doc{ - { - Name: "enabled", - Type: "bool", - Note: "", - Description: "Enable host DNS caching resolver.", - Comments: [3]string{"" /* encoder.HeadComment */, "Enable host DNS caching resolver." /* encoder.LineComment */, "" /* encoder.FootComment */}, - }, - { - Name: "forwardKubeDNSToHost", - Type: "bool", - Note: "", - Description: "Use the host DNS resolver as upstream for Kubernetes CoreDNS pods.\n\nWhen enabled, CoreDNS pods use host DNS server as the upstream DNS (instead of\nusing configured upstream DNS resolvers directly).", - Comments: [3]string{"" /* encoder.HeadComment */, "Use the host DNS resolver as upstream for Kubernetes CoreDNS pods." /* encoder.LineComment */, "" /* encoder.FootComment */}, - }, - { - Name: "resolveMemberNames", - Type: "bool", - Note: "", - Description: "Resolve member hostnames using the host DNS resolver.\n\nWhen enabled, cluster member hostnames and node names are resolved using the host DNS resolver.\nThis requires service discovery to be enabled.", - Comments: [3]string{"" /* encoder.HeadComment */, "Resolve member hostnames using the host DNS resolver." /* encoder.LineComment */, "" /* encoder.FootComment */}, - }, - }, - } - - return doc -} - func (VolumeMountConfig) Doc() *encoder.Doc { doc := &encoder.Doc{ Type: "VolumeMountConfig", @@ -2459,7 +2414,6 @@ func GetFileDoc() *encoder.FileDoc { KubePrism{}.Doc(), ImageCacheConfig{}.Doc(), KubernetesTalosAPIAccessConfig{}.Doc(), - HostDNSConfig{}.Doc(), VolumeMountConfig{}.Doc(), ClusterInlineManifest{}.Doc(), ClusterDiscoveryConfig{}.Doc(), diff --git a/pkg/machinery/config/types/v1alpha1/v1alpha1_validation.go b/pkg/machinery/config/types/v1alpha1/v1alpha1_validation.go index 3f38cc5d4..0a20c85b1 100644 --- a/pkg/machinery/config/types/v1alpha1/v1alpha1_validation.go +++ b/pkg/machinery/config/types/v1alpha1/v1alpha1_validation.go @@ -117,19 +117,10 @@ func (c *Config) Validate(mode validation.RuntimeMode, options ...validation.Opt } } - if mode.InContainer() { - // require that HostDNS features are enabled to passthrough container DNS to kube-dns - if !c.Machine().Features().HostDNS().Enabled() { - result = multierror.Append(result, errors.New("feature HostDNS should be enabled in container mode (.machine.features.hostDNS.enabled)")) + if c.NetworkHostDNSConfig() != nil { + if c.NetworkHostDNSConfig().ForwardKubeDNSToHost() && !c.NetworkHostDNSConfig().HostDNSEnabled() { + result = multierror.Append(result, errors.New("feature hostDNS.forwardKubeDNSToHost requires hostDNS.enabled to be true (.machine.features.hostDNS)")) } - - if !c.Machine().Features().HostDNS().ForwardKubeDNSToHost() { - result = multierror.Append(result, errors.New("feature HostDNS should forward kube-dns to host in container mode (.machine.features.hostDNS.forwardKubeDNSToHost)")) - } - } - - if c.Machine().Features().HostDNS().ForwardKubeDNSToHost() && !c.Machine().Features().HostDNS().Enabled() { - result = multierror.Append(result, errors.New("feature hostDNS.forwardKubeDNSToHost requires hostDNS.enabled to be true (.machine.features.hostDNS)")) } if t := c.Machine().Type(); t != machine.TypeUnknown && t.String() != c.MachineConfig.MachineType { diff --git a/pkg/machinery/config/types/v1alpha1/v1alpha1_validation_test.go b/pkg/machinery/config/types/v1alpha1/v1alpha1_validation_test.go index 34bb1f037..2d8c5b101 100644 --- a/pkg/machinery/config/types/v1alpha1/v1alpha1_validation_test.go +++ b/pkg/machinery/config/types/v1alpha1/v1alpha1_validation_test.go @@ -2020,7 +2020,7 @@ func TestValidate(t *testing.T) { }, MachineFeatures: &v1alpha1.FeaturesConfig{ HostDNSSupport: &v1alpha1.HostDNSConfig{ - HostDNSEnabled: new(false), + HostDNSConfigEnabled: new(false), HostDNSForwardKubeDNSToHost: new(true), }, }, diff --git a/pkg/machinery/config/types/v1alpha1/zz_generated.deepcopy.go b/pkg/machinery/config/types/v1alpha1/zz_generated.deepcopy.go index bdf9d29bf..16f549972 100644 --- a/pkg/machinery/config/types/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/machinery/config/types/v1alpha1/zz_generated.deepcopy.go @@ -1114,8 +1114,8 @@ func (in *FlannelCNIConfig) DeepCopy() *FlannelCNIConfig { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HostDNSConfig) DeepCopyInto(out *HostDNSConfig) { *out = *in - if in.HostDNSEnabled != nil { - in, out := &in.HostDNSEnabled, &out.HostDNSEnabled + if in.HostDNSConfigEnabled != nil { + in, out := &in.HostDNSConfigEnabled, &out.HostDNSConfigEnabled *out = new(bool) **out = **in } diff --git a/website/content/v1.14/reference/configuration/network/resolverconfig.md b/website/content/v1.14/reference/configuration/network/resolverconfig.md index fb7b623e7..3cdd21b58 100644 --- a/website/content/v1.14/reference/configuration/network/resolverconfig.md +++ b/website/content/v1.14/reference/configuration/network/resolverconfig.md @@ -35,11 +35,22 @@ searchDomains: disableDefault: true # Disable default search domain configuration from hostname FQDN. {{< /highlight >}} +{{< highlight yaml >}} +apiVersion: v1alpha1 +kind: ResolverConfig +# Configuration for host DNS resolver. +hostDNS: + enabled: true # Enable host DNS caching resolver. + forwardKubeDNSToHost: true # Use the host DNS resolver as upstream for Kubernetes CoreDNS pods. + resolveMemberNames: true # Resolve member hostnames using the host DNS resolver. +{{< /highlight >}} + | Field | Type | Description | Value(s) | |-------|------|-------------|----------| |`nameservers` |[]NameserverConfig |A list of nameservers (DNS servers) to use for resolving domain names.

Nameservers are used to resolve domain names on the host, and they are also
propagated to Kubernetes DNS (CoreDNS) for use by pods running on the cluster.

This overrides any nameservers obtained via DHCP or platform configuration.
Default configuration is to use 1.1.1.1 and 8.8.8.8 as nameservers. | | |`searchDomains` |SearchDomainsConfig |Configuration for search domains (in /etc/resolv.conf).

The default is to derive search domains from the hostname FQDN. | | +|`hostDNS` |HostDNSConfig |Configuration for host DNS resolver.

This configures a local DNS caching resolver on the host to improve DNS resolution performance and reliability. | | @@ -79,5 +90,23 @@ SearchDomainsConfig represents search domains configuration. +## hostDNS {#ResolverConfig.hostDNS} + +HostDNSConfig represents host DNS configuration. + + + + +| Field | Type | Description | Value(s) | +|-------|------|-------------|----------| +|`enabled` |bool |Enable host DNS caching resolver.

When enabled, a local DNS caching resolver is deployed on the host to improve DNS resolution performance and reliability.
Upstream DNS servers for the host resolver are configured using the `nameservers` field in this config document. | | +|`forwardKubeDNSToHost` |bool |Use the host DNS resolver as upstream for Kubernetes CoreDNS pods.

When enabled, CoreDNS pods use host DNS server as the upstream DNS (instead of
using configured upstream DNS resolvers directly). | | +|`resolveMemberNames` |bool |Resolve member hostnames using the host DNS resolver.

When enabled, cluster member hostnames and node names are resolved using the host DNS resolver.
This requires service discovery to be enabled. | | + + + + + + diff --git a/website/content/v1.14/reference/configuration/v1alpha1/config.md b/website/content/v1.14/reference/configuration/v1alpha1/config.md index 0ac0b65da..c1b16844e 100644 --- a/website/content/v1.14/reference/configuration/v1alpha1/config.md +++ b/website/content/v1.14/reference/configuration/v1alpha1/config.md @@ -681,7 +681,6 @@ kubernetesTalosAPIAccess: {{< /highlight >}} | | |`diskQuotaSupport` |bool |Enable XFS project quota support for EPHEMERAL partition and user disks.
Also enables kubelet tracking of ephemeral disk usage in the kubelet via quota. | | |`kubePrism` |KubePrism |KubePrism - local proxy/load balancer on defined port that will distribute
requests to all API servers in the cluster. | | -|`hostDNS` |HostDNSConfig |Configures host DNS caching resolver. | | |`imageCache` |ImageCacheConfig |Enable Image Cache feature. | | |`nodeAddressSortAlgorithm` |string |Select the node address sort algorithm.
The 'v1' algorithm sorts addresses by the address itself.
The 'v2' algorithm prefers more specific prefixes.
If unset, defaults to 'v1'. | | @@ -736,24 +735,6 @@ KubePrism describes the configuration for the KubePrism load balancer. -#### hostDNS {#Config.machine.features.hostDNS} - -HostDNSConfig describes the configuration for the host DNS resolver. - - - - -| Field | Type | Description | Value(s) | -|-------|------|-------------|----------| -|`enabled` |bool |Enable host DNS caching resolver. | | -|`forwardKubeDNSToHost` |bool |Use the host DNS resolver as upstream for Kubernetes CoreDNS pods.

When enabled, CoreDNS pods use host DNS server as the upstream DNS (instead of
using configured upstream DNS resolvers directly). | | -|`resolveMemberNames` |bool |Resolve member hostnames using the host DNS resolver.

When enabled, cluster member hostnames and node names are resolved using the host DNS resolver.
This requires service discovery to be enabled. | | - - - - - - #### imageCache {#Config.machine.features.imageCache} ImageCacheConfig describes the configuration for the Image Cache feature. diff --git a/website/content/v1.14/schemas/config.schema.json b/website/content/v1.14/schemas/config.schema.json index 355c6380a..cfcf26649 100644 --- a/website/content/v1.14/schemas/config.schema.json +++ b/website/content/v1.14/schemas/config.schema.json @@ -2226,6 +2226,34 @@ ], "description": "HCloudVIPConfig is a config document to configure virtual IP using Hetzner Cloud APIs for announcement.\\nVirtual IP configuration should be used only on controlplane nodes to provide virtual IP for Kubernetes API server.\\nAny other use cases are not supported and may lead to unexpected behavior.\\nVirtual IP will be announced from only one node at a time using Hetzner Cloud APIs.\\n" }, + "network.HostDNSConfig": { + "properties": { + "enabled": { + "type": "boolean", + "title": "enabled", + "description": "Enable host DNS caching resolver.\n\nWhen enabled, a local DNS caching resolver is deployed on the host to improve DNS resolution performance and reliability.\nUpstream DNS servers for the host resolver are configured using the nameservers field in this config document.\n", + "markdownDescription": "Enable host DNS caching resolver.\n\nWhen enabled, a local DNS caching resolver is deployed on the host to improve DNS resolution performance and reliability.\nUpstream DNS servers for the host resolver are configured using the `nameservers` field in this config document.", + "x-intellij-html-description": "\u003cp\u003eEnable host DNS caching resolver.\u003c/p\u003e\n\n\u003cp\u003eWhen enabled, a local DNS caching resolver is deployed on the host to improve DNS resolution performance and reliability.\nUpstream DNS servers for the host resolver are configured using the \u003ccode\u003enameservers\u003c/code\u003e field in this config document.\u003c/p\u003e\n" + }, + "forwardKubeDNSToHost": { + "type": "boolean", + "title": "forwardKubeDNSToHost", + "description": "Use the host DNS resolver as upstream for Kubernetes CoreDNS pods.\n\nWhen enabled, CoreDNS pods use host DNS server as the upstream DNS (instead of\nusing configured upstream DNS resolvers directly).\n", + "markdownDescription": "Use the host DNS resolver as upstream for Kubernetes CoreDNS pods.\n\nWhen enabled, CoreDNS pods use host DNS server as the upstream DNS (instead of\nusing configured upstream DNS resolvers directly).", + "x-intellij-html-description": "\u003cp\u003eUse the host DNS resolver as upstream for Kubernetes CoreDNS pods.\u003c/p\u003e\n\n\u003cp\u003eWhen enabled, CoreDNS pods use host DNS server as the upstream DNS (instead of\nusing configured upstream DNS resolvers directly).\u003c/p\u003e\n" + }, + "resolveMemberNames": { + "type": "boolean", + "title": "resolveMemberNames", + "description": "Resolve member hostnames using the host DNS resolver.\n\nWhen enabled, cluster member hostnames and node names are resolved using the host DNS resolver.\nThis requires service discovery to be enabled.\n", + "markdownDescription": "Resolve member hostnames using the host DNS resolver.\n\nWhen enabled, cluster member hostnames and node names are resolved using the host DNS resolver.\nThis requires service discovery to be enabled.", + "x-intellij-html-description": "\u003cp\u003eResolve member hostnames using the host DNS resolver.\u003c/p\u003e\n\n\u003cp\u003eWhen enabled, cluster member hostnames and node names are resolved using the host DNS resolver.\nThis requires service discovery to be enabled.\u003c/p\u003e\n" + } + }, + "additionalProperties": false, + "type": "object", + "description": "HostDNSConfig represents host DNS configuration." + }, "network.HostnameConfigV1Alpha1": { "properties": { "apiVersion": { @@ -2705,6 +2733,13 @@ "description": "Configuration for search domains (in /etc/resolv.conf).\n\nThe default is to derive search domains from the hostname FQDN.\n", "markdownDescription": "Configuration for search domains (in /etc/resolv.conf).\n\nThe default is to derive search domains from the hostname FQDN.", "x-intellij-html-description": "\u003cp\u003eConfiguration for search domains (in /etc/resolv.conf).\u003c/p\u003e\n\n\u003cp\u003eThe default is to derive search domains from the hostname FQDN.\u003c/p\u003e\n" + }, + "hostDNS": { + "$ref": "#/$defs/network.HostDNSConfig", + "title": "hostDNS", + "description": "Configuration for host DNS resolver.\n\nThis configures a local DNS caching resolver on the host to improve DNS resolution performance and reliability.\n", + "markdownDescription": "Configuration for host DNS resolver.\n\nThis configures a local DNS caching resolver on the host to improve DNS resolution performance and reliability.", + "x-intellij-html-description": "\u003cp\u003eConfiguration for host DNS resolver.\u003c/p\u003e\n\n\u003cp\u003eThis configures a local DNS caching resolver on the host to improve DNS resolution performance and reliability.\u003c/p\u003e\n" } }, "additionalProperties": false, @@ -4674,13 +4709,6 @@ "markdownDescription": "KubePrism - local proxy/load balancer on defined port that will distribute\nrequests to all API servers in the cluster.", "x-intellij-html-description": "\u003cp\u003eKubePrism - local proxy/load balancer on defined port that will distribute\nrequests to all API servers in the cluster.\u003c/p\u003e\n" }, - "hostDNS": { - "$ref": "#/$defs/v1alpha1.HostDNSConfig", - "title": "hostDNS", - "description": "Configures host DNS caching resolver.\n", - "markdownDescription": "Configures host DNS caching resolver.", - "x-intellij-html-description": "\u003cp\u003eConfigures host DNS caching resolver.\u003c/p\u003e\n" - }, "imageCache": { "$ref": "#/$defs/v1alpha1.ImageCacheConfig", "title": "imageCache", @@ -4724,34 +4752,6 @@ "type": "object", "description": "FlannelCNIConfig represents the Flannel CNI configuration options." }, - "v1alpha1.HostDNSConfig": { - "properties": { - "enabled": { - "type": "boolean", - "title": "enabled", - "description": "Enable host DNS caching resolver.\n", - "markdownDescription": "Enable host DNS caching resolver.", - "x-intellij-html-description": "\u003cp\u003eEnable host DNS caching resolver.\u003c/p\u003e\n" - }, - "forwardKubeDNSToHost": { - "type": "boolean", - "title": "forwardKubeDNSToHost", - "description": "Use the host DNS resolver as upstream for Kubernetes CoreDNS pods.\n\nWhen enabled, CoreDNS pods use host DNS server as the upstream DNS (instead of\nusing configured upstream DNS resolvers directly).\n", - "markdownDescription": "Use the host DNS resolver as upstream for Kubernetes CoreDNS pods.\n\nWhen enabled, CoreDNS pods use host DNS server as the upstream DNS (instead of\nusing configured upstream DNS resolvers directly).", - "x-intellij-html-description": "\u003cp\u003eUse the host DNS resolver as upstream for Kubernetes CoreDNS pods.\u003c/p\u003e\n\n\u003cp\u003eWhen enabled, CoreDNS pods use host DNS server as the upstream DNS (instead of\nusing configured upstream DNS resolvers directly).\u003c/p\u003e\n" - }, - "resolveMemberNames": { - "type": "boolean", - "title": "resolveMemberNames", - "description": "Resolve member hostnames using the host DNS resolver.\n\nWhen enabled, cluster member hostnames and node names are resolved using the host DNS resolver.\nThis requires service discovery to be enabled.\n", - "markdownDescription": "Resolve member hostnames using the host DNS resolver.\n\nWhen enabled, cluster member hostnames and node names are resolved using the host DNS resolver.\nThis requires service discovery to be enabled.", - "x-intellij-html-description": "\u003cp\u003eResolve member hostnames using the host DNS resolver.\u003c/p\u003e\n\n\u003cp\u003eWhen enabled, cluster member hostnames and node names are resolved using the host DNS resolver.\nThis requires service discovery to be enabled.\u003c/p\u003e\n" - } - }, - "additionalProperties": false, - "type": "object", - "description": "HostDNSConfig describes the configuration for the host DNS resolver." - }, "v1alpha1.ImageCacheConfig": { "properties": { "localEnabled": {