Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions addons/operating-system-manager/deployment-controller.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,7 @@ spec:
- -log-format=json # json or console
- -health-probe-address=0.0.0.0:8085
- -metrics-address=0.0.0.0:8080
{{ if .Config.Features.NodeLocalDNS.Deploy -}}
- -cluster-dns={{ .Resources.NodeLocalDNSVirtualIP }}
{{ end -}}
- -cluster-dns={{ .ClusterDNSIP }}
- -namespace=kube-system
- -container-runtime={{ .Config.ContainerRuntime }}
- -pause-image={{ .InternalImages.Get "PauseImage" }}
Expand Down
3 changes: 2 additions & 1 deletion docs/api_reference/v1beta2.en.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
+++
title = "v1beta2 API Reference"
date = 2026-04-03T16:57:31+03:00
date = 2026-04-15T10:59:17+03:00
weight = 11
+++
## v1beta2
Expand Down Expand Up @@ -242,6 +242,7 @@ ClusterNetworkConfig describes the cluster network
| ipFamily | IPFamily allows specifying IP family of a cluster. Valid values are IPv4 \| IPv6 \| IPv4+IPv6 \| IPv6+IPv4. | IPFamily | false |
| nodeCIDRMaskSizeIPv4 | NodeCIDRMaskSizeIPv4 is the mask size used to address the nodes within provided IPv4 Pods CIDR. It has to be larger than the provided IPv4 Pods CIDR. Defaults to 24. | *int | false |
| nodeCIDRMaskSizeIPv6 | NodeCIDRMaskSizeIPv6 is the mask size used to address the nodes within provided IPv6 Pods CIDR. It has to be larger than the provided IPv6 Pods CIDR. Defaults to 64. | *int | false |
| dnsServiceIP | DNSServiceIP is an optional override for the cluster DNS service IP. If not set, it defaults to the 10th IP of the serviceSubnet (e.g. 10.96.0.10 for 10.96.0.0/12). When nodeLocalDNS is enabled, OSM always uses the NodeLocalDNS virtual IP regardless of this field. | string | false |

[Back to Group](#v1beta2)

Expand Down
3 changes: 2 additions & 1 deletion docs/api_reference/v1beta3.en.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
+++
title = "v1beta3 API Reference"
date = 2026-04-03T16:57:31+03:00
date = 2026-04-15T10:59:17+03:00
weight = 11
+++
## v1beta3
Expand Down Expand Up @@ -255,6 +255,7 @@ ClusterNetworkConfig describes the cluster network
| ipFamily | IPFamily allows specifying IP family of a cluster. Valid values are IPv4 \| IPv6 \| IPv4+IPv6 \| IPv6+IPv4. | IPFamily | false |
| nodeCIDRMaskSizeIPv4 | NodeCIDRMaskSizeIPv4 is the mask size used to address the nodes within provided IPv4 Pods CIDR. It has to be larger than the provided IPv4 Pods CIDR. Defaults to 24. | *int | false |
| nodeCIDRMaskSizeIPv6 | NodeCIDRMaskSizeIPv6 is the mask size used to address the nodes within provided IPv6 Pods CIDR. It has to be larger than the provided IPv6 Pods CIDR. Defaults to 64. | *int | false |
| dnsServiceIP | DNSServiceIP is an optional override for the cluster DNS service IP. If not set, it defaults to the 10th IP of the serviceSubnet (e.g. 10.96.0.10 for 10.96.0.0/12). When nodeLocalDNS is enabled, OSM always uses the NodeLocalDNS virtual IP regardless of this field. | string | false |

[Back to Group](#v1beta3)

Expand Down
14 changes: 12 additions & 2 deletions pkg/addons/applier.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ type templateData struct {
InternalImages *internalImages
Resources map[string]string
Params map[string]string
ClusterDNSIP string
}

type registryCredentialsContainer struct {
Expand Down Expand Up @@ -185,6 +186,14 @@ func newAddonsApplier(s *state.State) (*applier, error) {
}
}

clusterDNSIP := resources.NodeLocalDNSVirtualIP
if !s.Cluster.Features.NodeLocalDNS.Deploy {
clusterDNSIP, err = s.Cluster.ClusterNetwork.EffectiveDNSServiceIP()
if err != nil {
return nil, fail.Runtime(err, "computing cluster DNS service IP")
}
}

data := templateData{
Config: s.Cluster,
Certificates: map[string]string{
Expand All @@ -211,8 +220,9 @@ func newAddonsApplier(s *state.State) (*applier, error) {
pauseImage: s.PauseImage,
resolver: s.Images.Get,
},
Resources: resources.All(),
Params: map[string]string{},
Resources: resources.All(),
Params: map[string]string{},
ClusterDNSIP: clusterDNSIP,
}

if !s.LiveCluster.IsProvisioned() {
Expand Down
30 changes: 30 additions & 0 deletions pkg/apis/kubeone/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ package kubeone

import (
"bytes"
"encoding/binary"
"fmt"
"math/rand"
"net"
"net/url"
"path/filepath"
"sort"
Expand Down Expand Up @@ -547,3 +549,31 @@ func (v VersionConfig) KubernetesMajorMinorVersion() string {

return fmt.Sprintf("v%d.%d", kubeSemVer.Major(), kubeSemVer.Minor())
}

// EffectiveDNSServiceIP returns the cluster DNS service IP to use for kubelet and OSM.
// If DNSServiceIP is explicitly set, it is returned as-is.
// Otherwise the 10th IP in the ServiceSubnet is computed (e.g. 10.96.0.10 for 10.96.0.0/12),
// matching the CoreDNS service IP assigned by kubeadm.
func (c ClusterNetworkConfig) EffectiveDNSServiceIP() (string, error) {
if c.DNSServiceIP != "" {
return c.DNSServiceIP, nil
}

return nthIPInSubnet(c.ServiceSubnet, 10)
}

// nthIPInSubnet returns the n-th IP address within the given CIDR subnet.
func nthIPInSubnet(cidr string, n uint32) (string, error) {
_, network, err := net.ParseCIDR(cidr)
if err != nil {
return "", fmt.Errorf("parsing subnet %q: %w", cidr, err)
}

ip := network.IP.To4()
addr := binary.BigEndian.Uint32(ip)
addr += n
result := make(net.IP, 4)
binary.BigEndian.PutUint32(result, addr)

return result.String(), nil
}
5 changes: 5 additions & 0 deletions pkg/apis/kubeone/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,11 @@ type ClusterNetworkConfig struct {

// NodeCIDRMaskSizeIPv6 is the mask size used to address the nodes within provided IPv6 Pods CIDR. It has to be larger than the provided IPv6 Pods CIDR. Defaults to 64.
NodeCIDRMaskSizeIPv6 *int `json:"nodeCIDRMaskSizeIPv6,omitempty"`

// DNSServiceIP is an optional override for the cluster DNS service IP.
// If not set, it defaults to the 10th IP of the serviceSubnet (e.g. 10.96.0.10 for 10.96.0.0/12).
// When nodeLocalDNS is enabled, OSM always uses the NodeLocalDNS virtual IP regardless of this field.
DNSServiceIP string `json:"dnsServiceIP,omitempty"`
}

// IPFamily allows specifying IP family of a cluster.
Expand Down
5 changes: 5 additions & 0 deletions pkg/apis/kubeone/v1beta2/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,11 @@ type ClusterNetworkConfig struct {

// NodeCIDRMaskSizeIPv6 is the mask size used to address the nodes within provided IPv6 Pods CIDR. It has to be larger than the provided IPv6 Pods CIDR. Defaults to 64.
NodeCIDRMaskSizeIPv6 *int `json:"nodeCIDRMaskSizeIPv6,omitempty"`

// DNSServiceIP is an optional override for the cluster DNS service IP.
// If not set, it defaults to the 10th IP of the serviceSubnet (e.g. 10.96.0.10 for 10.96.0.0/12).
// When nodeLocalDNS is enabled, OSM always uses the NodeLocalDNS virtual IP regardless of this field.
DNSServiceIP string `json:"dnsServiceIP,omitempty"`
}

// IPFamily allows specifying IP family of a cluster.
Expand Down
2 changes: 2 additions & 0 deletions pkg/apis/kubeone/v1beta2/zz_generated.conversion.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions pkg/apis/kubeone/v1beta3/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,11 @@ type ClusterNetworkConfig struct {

// NodeCIDRMaskSizeIPv6 is the mask size used to address the nodes within provided IPv6 Pods CIDR. It has to be larger than the provided IPv6 Pods CIDR. Defaults to 64.
NodeCIDRMaskSizeIPv6 *int `json:"nodeCIDRMaskSizeIPv6,omitempty"`

// DNSServiceIP is an optional override for the cluster DNS service IP.
// If not set, it defaults to the 10th IP of the serviceSubnet (e.g. 10.96.0.10 for 10.96.0.0/12).
// When nodeLocalDNS is enabled, OSM always uses the NodeLocalDNS virtual IP regardless of this field.
DNSServiceIP string `json:"dnsServiceIP,omitempty"`
}

// IPFamily allows specifying IP family of a cluster.
Expand Down
2 changes: 2 additions & 0 deletions pkg/apis/kubeone/v1beta3/zz_generated.conversion.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions pkg/apis/kubeone/validation/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,19 @@ func ValidateClusterNetworkConfig(c kubeoneapi.ClusterNetworkConfig, prov kubeon
allErrs = append(allErrs, ValidateKubeProxy(c.KubeProxy, fldPath.Child("kubeProxy"))...)
}

if c.DNSServiceIP != "" {
ip := net.ParseIP(c.DNSServiceIP)
if ip == nil {
allErrs = append(allErrs, field.Invalid(fldPath.Child("dnsServiceIP"), c.DNSServiceIP, "must be a valid IP address"))
} else if c.ServiceSubnet != "" {
_, serviceSubnet, err := net.ParseCIDR(c.ServiceSubnet)
if err == nil && !serviceSubnet.Contains(ip) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("dnsServiceIP"), c.DNSServiceIP,
fmt.Sprintf("must be within the service subnet %q", c.ServiceSubnet)))
}
}
}

return allErrs
}

Expand Down
42 changes: 42 additions & 0 deletions pkg/apis/kubeone/validation/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1646,6 +1646,48 @@ func TestValidateClusterNetworkConfig(t *testing.T) {
},
expectedError: true,
},
{
name: "valid dnsServiceIP within service subnet",
clusterNetworkConfig: kubeoneapi.ClusterNetworkConfig{
PodSubnet: "192.168.1.0/16",
ServiceSubnet: "10.96.0.0/12",
IPFamily: kubeoneapi.IPFamilyIPv4,
NodeCIDRMaskSizeIPv4: new(24),
DNSServiceIP: "10.96.0.100",
},
provider: kubeoneapi.CloudProviderSpec{
None: &kubeoneapi.NoneSpec{},
},
expectedError: false,
},
{
name: "invalid dnsServiceIP not within service subnet",
clusterNetworkConfig: kubeoneapi.ClusterNetworkConfig{
PodSubnet: "192.168.1.0/16",
ServiceSubnet: "10.96.0.0/12",
IPFamily: kubeoneapi.IPFamilyIPv4,
NodeCIDRMaskSizeIPv4: new(24),
DNSServiceIP: "192.168.1.1",
},
provider: kubeoneapi.CloudProviderSpec{
None: &kubeoneapi.NoneSpec{},
},
expectedError: true,
},
{
name: "invalid dnsServiceIP not a valid IP",
clusterNetworkConfig: kubeoneapi.ClusterNetworkConfig{
PodSubnet: "192.168.1.0/16",
ServiceSubnet: "10.96.0.0/12",
IPFamily: kubeoneapi.IPFamilyIPv4,
NodeCIDRMaskSizeIPv4: new(24),
DNSServiceIP: "not-an-ip",
},
provider: kubeoneapi.CloudProviderSpec{
None: &kubeoneapi.NoneSpec{},
},
expectedError: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
Expand Down
2 changes: 2 additions & 0 deletions pkg/templates/kubernetesconfigs/kubelet.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ func NewKubeletConfiguration(cluster *kubeoneapi.KubeOneCluster, featureGates ma

if cluster.Features.NodeLocalDNS.Deploy {
kubeletConfig.ClusterDNS = []string{resources.NodeLocalDNSVirtualIP}
} else if dnsip, err := cluster.ClusterNetwork.EffectiveDNSServiceIP(); err == nil && dnsip != "" {
kubeletConfig.ClusterDNS = []string{dnsip}
}

return dropFields(kubeletConfig,
Expand Down