Skip to content
Merged
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
6 changes: 6 additions & 0 deletions pkg/controller/kubelet-config/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,12 @@ func validateUserKubeletConfig(cfg *mcfgv1.KubeletConfig) error {
if kcDecoded.StaticPodPath != "" {
return fmt.Errorf("KubeletConfiguration: staticPodPath is not allowed to be set, but contains: %s", kcDecoded.StaticPodPath)
}
if kcDecoded.FailSwapOn != nil {
return fmt.Errorf("KubeletConfiguration: failSwapOn is not allowed to be set, but contains: %v", *kcDecoded.FailSwapOn)
}
if kcDecoded.MemorySwap.SwapBehavior != "" {
return fmt.Errorf("KubeletConfiguration: swapBehavior is not allowed to be set, but contains: %s", kcDecoded.MemorySwap.SwapBehavior)
}
if kcDecoded.SystemReserved != nil && len(kcDecoded.SystemReserved) > 0 &&
cfg.Spec.AutoSizingReserved != nil && *cfg.Spec.AutoSizingReserved {
return fmt.Errorf("KubeletConfiguration: autoSizingReserved and systemdReserved cannot be set together")
Expand Down
14 changes: 14 additions & 0 deletions pkg/controller/kubelet-config/kubelet_config_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,20 @@ func TestKubeletConfigDenylistedOptions(t *testing.T) {
},
},
},
{
name: "test banned failSwapOn",
config: &kubeletconfigv1beta1.KubeletConfiguration{
FailSwapOn: pointer.BoolPtr(true),
},
},
{
name: "test banned swapBehavior",
config: &kubeletconfigv1beta1.KubeletConfiguration{
MemorySwap: kubeletconfigv1beta1.MemorySwapConfiguration{
SwapBehavior: "NoSwap",
},
},
},
}

successTests := []struct {
Expand Down
55 changes: 55 additions & 0 deletions pkg/controller/kubelet-config/kubelet_config_failswapon_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package kubeletconfig

import (
"testing"

osev1 "github.com/openshift/api/config/v1"
ctrlcommon "github.com/openshift/machine-config-operator/pkg/controller/common"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestFailSwapOnConfiguration verifies that failSwapOn is correctly configured in kubelet templates.
// This test ensures that:
// - Master and arbiter nodes have failSwapOn: true (swap is disabled)
// - Worker nodes have failSwapOn: false (swap is allowed but controlled via memorySwap.swapBehavior)
// - All nodes have memorySwap.swapBehavior set to "NoSwap"
func TestFailSwapOnConfiguration(t *testing.T) {
for _, platform := range []osev1.PlatformType{osev1.AWSPlatformType, osev1.NonePlatformType, "unrecognized"} {
t.Run(string(platform), func(t *testing.T) {
f := newFixture(t)
fgHandler := ctrlcommon.NewFeatureGatesHardcodedHandler([]osev1.FeatureGateName{"Example"}, nil)

cc := newControllerConfig(ctrlcommon.ControllerConfigName, platform)
f.ccLister = append(f.ccLister, cc)

ctrl := f.newController(fgHandler)
testCases := []struct {
nodeRole string
expectedFail bool
}{
{"master", true},
{"arbiter", true},
{"worker", false},
}

for _, tc := range testCases {
t.Run(tc.nodeRole, func(t *testing.T) {
kubeletConfig, err := generateOriginalKubeletConfigIgn(cc, ctrl.templatesDir, tc.nodeRole, &osev1.APIServer{})
require.NoError(t, err, "Failed to generate kubelet config for %s", tc.nodeRole)

contents, err := ctrlcommon.DecodeIgnitionFileContents(kubeletConfig.Contents.Source, kubeletConfig.Contents.Compression)
require.NoError(t, err, "Failed to decode ignition file contents for %s", tc.nodeRole)

originalKubeConfig, err := DecodeKubeletConfig(contents)
require.NoError(t, err, "Failed to decode kubelet config for %s", tc.nodeRole)

require.NotNil(t, originalKubeConfig.FailSwapOn, "failSwapOn should not be nil for %s node role", tc.nodeRole)
assert.Equal(t, tc.expectedFail, *originalKubeConfig.FailSwapOn, "failSwapOn should be %v for %s node role", tc.expectedFail, tc.nodeRole)

assert.Equal(t, "NoSwap", originalKubeConfig.MemorySwap.SwapBehavior, "memorySwap.swapBehavior should be NoSwap for %s node role", tc.nodeRole)
})
}
})
}
}
187 changes: 187 additions & 0 deletions pkg/controller/template/kubelet_config_dir_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package template

import (
"strings"
"testing"

configv1 "github.com/openshift/api/config/v1"
mcfgv1 "github.com/openshift/api/machineconfiguration/v1"
ctrlcommon "github.com/openshift/machine-config-operator/pkg/controller/common"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestKubeletConfigDirParameter verifies that the --config-dir parameter is correctly configured
// in kubelet.service systemd unit files for worker nodes only.
// This test ensures that:
// - Worker nodes have --config-dir=/etc/openshift/kubelet.conf.d in their kubelet.service
// - Master and arbiter nodes do NOT have the --config-dir parameter
//
// The --config-dir parameter allows the kubelet to load additional configuration from a directory,
// which is useful for dynamic kubelet configuration updates on worker nodes.
func TestKubeletConfigDirParameter(t *testing.T) {
testCases := []struct {
name string
platform configv1.PlatformType
controllerConfig string
expectedWorkerFlag bool
expectedMasterFlag bool
}{
{
name: "AWS",
platform: configv1.AWSPlatformType,
controllerConfig: "./test_data/controller_config_aws.yaml",
expectedWorkerFlag: true,
expectedMasterFlag: false,
},
{
name: "BareMetal",
platform: configv1.BareMetalPlatformType,
controllerConfig: "./test_data/controller_config_baremetal.yaml",
expectedWorkerFlag: true,
expectedMasterFlag: false,
},
{
name: "GCP",
platform: configv1.GCPPlatformType,
controllerConfig: "./test_data/controller_config_gcp.yaml",
expectedWorkerFlag: true,
expectedMasterFlag: false,
},
{
name: "None",
platform: configv1.NonePlatformType,
controllerConfig: "./test_data/controller_config_none.yaml",
expectedWorkerFlag: true,
expectedMasterFlag: false,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
controllerConfig, err := controllerConfigFromFile(tc.controllerConfig)
require.NoError(t, err, "Failed to load controller config for %s", tc.name)

cfgs, err := generateTemplateMachineConfigs(&RenderConfig{&controllerConfig.Spec, `{"dummy":"dummy"}`, "dummy", nil, nil}, templateDir)
require.NoError(t, err, "Failed to generate machine configs for %s", tc.name)

kubeletConfigs := make(map[string]*string)

for _, cfg := range cfgs {
role, ok := cfg.Labels[mcfgv1.MachineConfigRoleLabelKey]
require.True(t, ok, "Machine config missing role label")

ign, err := ctrlcommon.ParseAndConvertConfig(cfg.Spec.Config.Raw)
require.NoError(t, err, "Failed to parse Ignition config for %s/%s", tc.name, role)

for _, unit := range ign.Systemd.Units {
if unit.Name == "kubelet.service" && unit.Contents != nil {
kubeletConfigs[role] = unit.Contents
break
}
}
}

for role, kubeletUnit := range kubeletConfigs {

hasConfigDirFlag := strings.Contains(*kubeletUnit, "--config-dir=/etc/openshift/kubelet.conf.d")

switch role {
case workerRole:
if tc.expectedWorkerFlag {
assert.True(t, hasConfigDirFlag,
"Worker node should have --config-dir parameter for platform %s", tc.name)
} else {
assert.False(t, hasConfigDirFlag,
"Worker node should not have --config-dir parameter for platform %s", tc.name)
}
case masterRole:
if tc.expectedMasterFlag {
assert.True(t, hasConfigDirFlag,
"Master node should have --config-dir parameter for platform %s", tc.name)
} else {
assert.False(t, hasConfigDirFlag,
"Master node should not have --config-dir parameter for platform %s", tc.name)
}
case arbiterRole:
if tc.expectedMasterFlag {
assert.True(t, hasConfigDirFlag,
"Arbiter node should have --config-dir parameter for platform %s", tc.name)
} else {
assert.False(t, hasConfigDirFlag,
"Arbiter node should not have --config-dir parameter for platform %s", tc.name)
}
}

if hasConfigDirFlag {
assert.Contains(t, *kubeletUnit, "--config-dir=/etc/openshift/kubelet.conf.d",
"--config-dir should point to /etc/openshift/kubelet.conf.d for %s/%s", tc.name, role)
}
}
})
}
}

// TestKubeletConfigDirParameterSpecific tests the exact location and format of the --config-dir parameter
func TestKubeletConfigDirParameterSpecific(t *testing.T) {
controllerConfig, err := controllerConfigFromFile("./test_data/controller_config_aws.yaml")
require.NoError(t, err, "Failed to load AWS controller config")

cfgs, err := generateTemplateMachineConfigs(&RenderConfig{&controllerConfig.Spec, `{"dummy":"dummy"}`, "dummy", nil, nil}, templateDir)
require.NoError(t, err, "Failed to generate machine configs")

var kubeletUnit *string
for _, cfg := range cfgs {
if role, ok := cfg.Labels[mcfgv1.MachineConfigRoleLabelKey]; ok && role == workerRole {
ign, err := ctrlcommon.ParseAndConvertConfig(cfg.Spec.Config.Raw)
require.NoError(t, err, "Failed to parse worker Ignition config")

for _, unit := range ign.Systemd.Units {
if unit.Name == "kubelet.service" && unit.Contents != nil {
kubeletUnit = unit.Contents
break
}
}
if kubeletUnit != nil {
break
}
}
}

require.NotNil(t, kubeletUnit, "kubelet.service unit not found in worker config")

lines := strings.Split(*kubeletUnit, "\n")
var execStartLines []string

inExecStart := false
for _, line := range lines {
trimmedLine := strings.TrimSpace(line)
if strings.HasPrefix(trimmedLine, "ExecStart=") {
inExecStart = true
execStartLines = append(execStartLines, trimmedLine)
} else if inExecStart && strings.HasSuffix(trimmedLine, "\\") {
execStartLines = append(execStartLines, trimmedLine)
} else if inExecStart && !strings.HasSuffix(trimmedLine, "\\") {
execStartLines = append(execStartLines, trimmedLine)
break
}
}

require.NotEmpty(t, execStartLines, "ExecStart lines not found in kubelet.service")

fullExecStart := strings.Join(execStartLines, " ")

assert.Contains(t, fullExecStart, "--config-dir=/etc/openshift/kubelet.conf.d",
"Worker kubelet.service should contain --config-dir=/etc/openshift/kubelet.conf.d")

configIndex := strings.Index(fullExecStart, "--config=/etc/kubernetes/kubelet.conf")
configDirIndex := strings.Index(fullExecStart, "--config-dir=/etc/openshift/kubelet.conf.d")
bootstrapIndex := strings.Index(fullExecStart, "--bootstrap-kubeconfig=/etc/kubernetes/kubeconfig")

assert.Greater(t, configDirIndex, configIndex,
"--config-dir should appear after --config parameter")
assert.Less(t, configDirIndex, bootstrapIndex,
"--config-dir should appear before --bootstrap-kubeconfig parameter")

t.Logf("Worker kubelet.service ExecStart:\n%s", fullExecStart)
}
3 changes: 3 additions & 0 deletions templates/arbiter/01-arbiter-kubelet/_base/files/kubelet.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ contents:
clusterDomain: cluster.local
containerLogMaxSize: 50Mi
enableSystemLogQuery: true
failSwapOn: true
maxPods: 250
kubeAPIQPS: 50
kubeAPIBurst: 100
Expand All @@ -24,6 +25,8 @@ contents:
rotateCertificates: true
serializeImagePulls: false
staticPodPath: /etc/kubernetes/manifests
memorySwap:
swapBehavior: NoSwap
systemCgroups: /system.slice
nodeStatusUpdateFrequency: 10s
nodeStatusReportFrequency: 5m
Expand Down
3 changes: 3 additions & 0 deletions templates/master/01-master-kubelet/_base/files/kubelet.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ contents:
clusterDomain: cluster.local
containerLogMaxSize: 50Mi
enableSystemLogQuery: true
failSwapOn: true
maxPods: 250
kubeAPIQPS: 50
kubeAPIBurst: 100
Expand All @@ -24,6 +25,8 @@ contents:
rotateCertificates: true
serializeImagePulls: false
staticPodPath: /etc/kubernetes/manifests
memorySwap:
swapBehavior: NoSwap
systemCgroups: /system.slice
nodeStatusUpdateFrequency: 10s
nodeStatusReportFrequency: 5m
Expand Down
3 changes: 3 additions & 0 deletions templates/worker/01-worker-kubelet/_base/files/kubelet.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ contents:
clusterDomain: cluster.local
containerLogMaxSize: 50Mi
enableSystemLogQuery: true
failSwapOn: false
maxPods: 250
kubeAPIQPS: 50
kubeAPIBurst: 100
Expand All @@ -24,6 +25,8 @@ contents:
rotateCertificates: true
serializeImagePulls: false
staticPodPath: /etc/kubernetes/manifests
memorySwap:
swapBehavior: NoSwap
systemCgroups: /system.slice
nodeStatusUpdateFrequency: 10s
nodeStatusReportFrequency: 5m
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ contents: |
[Service]
Type=notify
ExecStartPre=/bin/mkdir --parents /etc/kubernetes/manifests
ExecStartPre=/bin/mkdir --parents /etc/openshift/kubelet.conf.d
ExecStartPre=-/usr/sbin/restorecon -ri /var/lib/kubelet/pod-resources /usr/local/bin/kubenswrapper /usr/bin/kubensenter
{{- if eq .IPFamilies "IPv6"}}
Environment="KUBELET_NODE_IP=::"
Expand All @@ -24,6 +25,7 @@ contents: |
ExecStart=/usr/local/bin/kubenswrapper \
/usr/bin/kubelet \
--config=/etc/kubernetes/kubelet.conf \
--config-dir=/etc/openshift/kubelet.conf.d \
--bootstrap-kubeconfig=/etc/kubernetes/kubeconfig \
--kubeconfig=/var/lib/kubelet/kubeconfig \
--container-runtime-endpoint=/var/run/crio/crio.sock \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ contents: |
[Service]
Type=notify
ExecStartPre=/bin/mkdir --parents /etc/kubernetes/manifests
ExecStartPre=/bin/mkdir --parents /etc/openshift/kubelet.conf.d
ExecStartPre=-/usr/sbin/restorecon -ri /var/lib/kubelet/pod-resources /usr/local/bin/kubenswrapper /usr/bin/kubensenter
{{- if eq .IPFamilies "IPv6"}}
Environment="KUBELET_NODE_IP=::"
Expand All @@ -24,6 +25,7 @@ contents: |
ExecStart=/usr/local/bin/kubenswrapper \
/usr/bin/kubelet \
--config=/etc/kubernetes/kubelet.conf \
--config-dir=/etc/openshift/kubelet.conf.d \
--bootstrap-kubeconfig=/etc/kubernetes/kubeconfig \
--kubeconfig=/var/lib/kubelet/kubeconfig \
--container-runtime-endpoint=/var/run/crio/crio.sock \
Expand Down