Skip to content

Commit 953afbb

Browse files
authored
Merge pull request kubernetes#121193 from sohankunkerkar/kubelet-config-dir
Retarget drop-in kubelet configuration dir feature to Alpha
2 parents 263ab25 + ad7b9b5 commit 953afbb

File tree

10 files changed

+265
-54
lines changed

10 files changed

+265
-54
lines changed

cmd/kubelet/app/server.go

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ package app
2020
import (
2121
"context"
2222
"crypto/tls"
23+
"encoding/json"
2324
"errors"
2425
"fmt"
2526
"io"
@@ -34,7 +35,7 @@ import (
3435
"time"
3536

3637
"github.com/coreos/go-systemd/v22/daemon"
37-
"github.com/imdario/mergo"
38+
jsonpatch "github.com/evanphx/json-patch"
3839
"github.com/spf13/cobra"
3940
"github.com/spf13/pflag"
4041
"google.golang.org/grpc/codes"
@@ -312,30 +313,34 @@ is checked every 20 seconds (also configurable with a flag).`,
312313
// potentially overriding the previous values.
313314
func mergeKubeletConfigurations(kubeletConfig *kubeletconfiginternal.KubeletConfiguration, kubeletDropInConfigDir string) error {
314315
const dropinFileExtension = ".conf"
315-
316+
baseKubeletConfigJSON, err := json.Marshal(kubeletConfig)
317+
if err != nil {
318+
return fmt.Errorf("failed to marshal base config: %w", err)
319+
}
316320
// Walk through the drop-in directory and update the configuration for each file
317-
err := filepath.WalkDir(kubeletDropInConfigDir, func(path string, info fs.DirEntry, err error) error {
321+
if err := filepath.WalkDir(kubeletDropInConfigDir, func(path string, info fs.DirEntry, err error) error {
318322
if err != nil {
319323
return err
320324
}
321325
if !info.IsDir() && filepath.Ext(info.Name()) == dropinFileExtension {
322-
dropinConfig, err := loadConfigFile(path)
326+
dropinConfigJSON, err := loadDropinConfigFileIntoJSON(path)
323327
if err != nil {
324328
return fmt.Errorf("failed to load kubelet dropin file, path: %s, error: %w", path, err)
325329
}
326-
327-
// Merge dropinConfig with kubeletConfig
328-
if err := mergo.Merge(kubeletConfig, dropinConfig, mergo.WithOverride); err != nil {
329-
return fmt.Errorf("failed to merge kubelet drop-in config, path: %s, error: %w", path, err)
330+
mergedConfigJSON, err := jsonpatch.MergePatch(baseKubeletConfigJSON, dropinConfigJSON)
331+
if err != nil {
332+
return fmt.Errorf("failed to merge drop-in and current config: %w", err)
330333
}
334+
baseKubeletConfigJSON = mergedConfigJSON
331335
}
332336
return nil
333-
})
334-
335-
if err != nil {
337+
}); err != nil {
336338
return fmt.Errorf("failed to walk through kubelet dropin directory %q: %w", kubeletDropInConfigDir, err)
337339
}
338340

341+
if err := json.Unmarshal(baseKubeletConfigJSON, kubeletConfig); err != nil {
342+
return fmt.Errorf("failed to unmarshal merged JSON into kubelet configuration: %w", err)
343+
}
339344
return nil
340345
}
341346

@@ -415,6 +420,20 @@ func loadConfigFile(name string) (*kubeletconfiginternal.KubeletConfiguration, e
415420
return kc, err
416421
}
417422

423+
func loadDropinConfigFileIntoJSON(name string) ([]byte, error) {
424+
const errFmt = "failed to load drop-in kubelet config file %s, error %v"
425+
// compute absolute path based on current working dir
426+
kubeletConfigFile, err := filepath.Abs(name)
427+
if err != nil {
428+
return nil, fmt.Errorf(errFmt, name, err)
429+
}
430+
loader, err := configfiles.NewFsLoader(&utilfs.DefaultFs{}, kubeletConfigFile)
431+
if err != nil {
432+
return nil, fmt.Errorf(errFmt, name, err)
433+
}
434+
return loader.LoadIntoJSON()
435+
}
436+
418437
// UnsecuredDependencies returns a Dependencies suitable for being run, or an error if the server setup
419438
// is not valid. It will not start any background processes, and does not include authentication/authorization
420439
func UnsecuredDependencies(s *options.KubeletServer, featureGate featuregate.FeatureGate) (*kubelet.Dependencies, error) {

cmd/kubelet/app/server_test.go

Lines changed: 45 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ import (
2121
"path/filepath"
2222
"reflect"
2323
"testing"
24+
"time"
2425

2526
"github.com/stretchr/testify/require"
27+
"gopkg.in/yaml.v2"
28+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2629
"k8s.io/kubernetes/cmd/kubelet/app/options"
2730
kubeletconfiginternal "k8s.io/kubernetes/pkg/kubelet/apis/config"
2831
)
@@ -71,20 +74,22 @@ func TestValueOfAllocatableResources(t *testing.T) {
7174

7275
func TestMergeKubeletConfigurations(t *testing.T) {
7376
testCases := []struct {
74-
kubeletConfig string
77+
kubeletConfig *kubeletconfiginternal.KubeletConfiguration
7578
dropin1 string
7679
dropin2 string
7780
overwrittenConfigFields map[string]interface{}
7881
cliArgs []string
7982
name string
8083
}{
8184
{
82-
kubeletConfig: `
83-
apiVersion: kubelet.config.k8s.io/v1beta1
84-
kind: KubeletConfiguration
85-
port: 9080
86-
readOnlyPort: 10257
87-
`,
85+
kubeletConfig: &kubeletconfiginternal.KubeletConfiguration{
86+
TypeMeta: metav1.TypeMeta{
87+
Kind: "KubeletConfiguration",
88+
APIVersion: "kubelet.config.k8s.io/v1beta1",
89+
},
90+
Port: int32(9090),
91+
ReadOnlyPort: int32(10257),
92+
},
8893
dropin1: `
8994
apiVersion: kubelet.config.k8s.io/v1beta1
9095
kind: KubeletConfiguration
@@ -103,13 +108,15 @@ readOnlyPort: 10255
103108
name: "kubelet.conf.d overrides kubelet.conf",
104109
},
105110
{
106-
kubeletConfig: `
107-
apiVersion: kubelet.config.k8s.io/v1beta1
108-
kind: KubeletConfiguration
109-
readOnlyPort: 10256
110-
kubeReserved:
111-
memory: 70Mi
112-
`,
111+
kubeletConfig: &kubeletconfiginternal.KubeletConfiguration{
112+
TypeMeta: metav1.TypeMeta{
113+
Kind: "KubeletConfiguration",
114+
APIVersion: "kubelet.config.k8s.io/v1beta1",
115+
},
116+
ReadOnlyPort: int32(10256),
117+
KubeReserved: map[string]string{"memory": "100Mi"},
118+
SyncFrequency: metav1.Duration{Duration: 5 * time.Minute},
119+
},
113120
dropin1: `
114121
apiVersion: kubelet.config.k8s.io/v1beta1
115122
kind: KubeletConfiguration
@@ -131,18 +138,19 @@ kubeReserved:
131138
"cpu": "200m",
132139
"memory": "100Mi",
133140
},
141+
"SyncFrequency": metav1.Duration{Duration: 5 * time.Minute},
134142
},
135143
name: "kubelet.conf.d overrides kubelet.conf with subfield override",
136144
},
137145
{
138-
kubeletConfig: `
139-
apiVersion: kubelet.config.k8s.io/v1beta1
140-
kind: KubeletConfiguration
141-
port: 9090
142-
clusterDNS:
143-
- 192.168.1.3
144-
- 192.168.1.4
145-
`,
146+
kubeletConfig: &kubeletconfiginternal.KubeletConfiguration{
147+
TypeMeta: metav1.TypeMeta{
148+
Kind: "KubeletConfiguration",
149+
APIVersion: "kubelet.config.k8s.io/v1beta1",
150+
},
151+
Port: int32(9090),
152+
ClusterDNS: []string{"192.168.1.3", "192.168.1.4"},
153+
},
146154
dropin1: `
147155
apiVersion: kubelet.config.k8s.io/v1beta1
148156
kind: KubeletConfiguration
@@ -173,6 +181,7 @@ clusterDNS:
173181
name: "kubelet.conf.d overrides kubelet.conf with slices/lists",
174182
},
175183
{
184+
kubeletConfig: nil,
176185
dropin1: `
177186
apiVersion: kubelet.config.k8s.io/v1beta1
178187
kind: KubeletConfiguration
@@ -195,13 +204,14 @@ readOnlyPort: 10255
195204
name: "cli args override kubelet.conf.d",
196205
},
197206
{
198-
kubeletConfig: `
199-
apiVersion: kubelet.config.k8s.io/v1beta1
200-
kind: KubeletConfiguration
201-
port: 9090
202-
clusterDNS:
203-
- 192.168.1.3
204-
`,
207+
kubeletConfig: &kubeletconfiginternal.KubeletConfiguration{
208+
TypeMeta: metav1.TypeMeta{
209+
Kind: "KubeletConfiguration",
210+
APIVersion: "kubelet.config.k8s.io/v1beta1",
211+
},
212+
Port: int32(9090),
213+
ClusterDNS: []string{"192.168.1.3"},
214+
},
205215
overwrittenConfigFields: map[string]interface{}{
206216
"Port": int32(9090),
207217
"ClusterDNS": []string{"192.168.1.2"},
@@ -222,12 +232,15 @@ clusterDNS:
222232
kubeletConfig := &kubeletconfiginternal.KubeletConfiguration{}
223233
kubeletFlags := &options.KubeletFlags{}
224234

225-
if len(test.kubeletConfig) > 0 {
235+
if test.kubeletConfig != nil {
226236
// Create the Kubeletconfig
227237
kubeletConfFile := filepath.Join(tempDir, "kubelet.conf")
228-
err := os.WriteFile(kubeletConfFile, []byte(test.kubeletConfig), 0644)
229-
require.NoError(t, err, "failed to create config from a yaml file")
238+
yamlData, err := yaml.Marshal(test.kubeletConfig) // Convert struct to YAML
239+
require.NoError(t, err, "failed to convert kubelet config to YAML")
240+
err = os.WriteFile(kubeletConfFile, yamlData, 0644)
241+
require.NoError(t, err, "failed to create config from YAML data")
230242
kubeletFlags.KubeletConfigFile = kubeletConfFile
243+
kubeletConfig = test.kubeletConfig
231244
}
232245
if len(test.dropin1) > 0 || len(test.dropin2) > 0 {
233246
// Create kubelet.conf.d directory and drop-in configuration files

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ require (
4545
github.com/google/go-cmp v0.6.0
4646
github.com/google/gofuzz v1.2.0
4747
github.com/google/uuid v1.3.0
48-
github.com/imdario/mergo v0.3.6
4948
github.com/ishidawataru/sctp v0.0.0-20230406120618-7ff4192f6ff2
5049
github.com/libopenstorage/openstorage v1.0.0
5150
github.com/lithammer/dedent v1.1.0
@@ -186,6 +185,7 @@ require (
186185
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
187186
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
188187
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
188+
github.com/imdario/mergo v0.3.6 // indirect
189189
github.com/inconshreveable/mousetrap v1.1.0 // indirect
190190
github.com/jonboulle/clockwork v0.2.2 // indirect
191191
github.com/josharian/intern v1.0.0 // indirect

pkg/kubelet/kubeletconfig/configfiles/configfiles.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ import (
3131
type Loader interface {
3232
// Load loads and returns the KubeletConfiguration from the storage layer, or an error if a configuration could not be loaded
3333
Load() (*kubeletconfig.KubeletConfiguration, error)
34+
// LoadIntoJSON loads and returns the KubeletConfiguration from the storage layer, or an error if a configuration could not be
35+
// loaded. It returns the configuration as a JSON byte slice
36+
LoadIntoJSON() ([]byte, error)
3437
}
3538

3639
// fsLoader loads configuration from `configDir`
@@ -78,6 +81,20 @@ func (loader *fsLoader) Load() (*kubeletconfig.KubeletConfiguration, error) {
7881
return kc, nil
7982
}
8083

84+
func (loader *fsLoader) LoadIntoJSON() ([]byte, error) {
85+
data, err := loader.fs.ReadFile(loader.kubeletFile)
86+
if err != nil {
87+
return nil, fmt.Errorf("failed to read drop-in kubelet config file %q, error: %v", loader.kubeletFile, err)
88+
}
89+
90+
// no configuration is an error, some parameters are required
91+
if len(data) == 0 {
92+
return nil, fmt.Errorf("kubelet config file %q was empty", loader.kubeletFile)
93+
}
94+
95+
return utilcodec.DecodeKubeletConfigurationIntoJSON(loader.kubeletCodecs, data)
96+
}
97+
8198
// resolveRelativePaths makes relative paths absolute by resolving them against `root`
8299
func resolveRelativePaths(paths []*string, root string) {
83100
for _, path := range paths {

pkg/kubelet/kubeletconfig/util/codec/codec.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ limitations under the License.
1717
package codec
1818

1919
import (
20+
"encoding/json"
2021
"fmt"
2122

2223
"k8s.io/klog/v2"
2324

2425
// ensure the core apis are installed
2526
_ "k8s.io/kubernetes/pkg/apis/core/install"
2627

28+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2729
"k8s.io/apimachinery/pkg/runtime"
2830
"k8s.io/apimachinery/pkg/runtime/schema"
2931
"k8s.io/apimachinery/pkg/runtime/serializer"
@@ -105,3 +107,16 @@ func DecodeKubeletConfiguration(kubeletCodecs *serializer.CodecFactory, data []b
105107

106108
return internalKC, nil
107109
}
110+
111+
// DecodeKubeletConfigurationIntoJSON decodes a serialized KubeletConfiguration to the internal type.
112+
func DecodeKubeletConfigurationIntoJSON(kubeletCodecs *serializer.CodecFactory, data []byte) ([]byte, error) {
113+
// The UniversalDecoder runs defaulting and returns the internal type by default.
114+
obj, _, err := kubeletCodecs.UniversalDecoder().Decode(data, nil, &unstructured.Unstructured{})
115+
if err != nil {
116+
return nil, err
117+
}
118+
119+
objT := obj.(*unstructured.Unstructured)
120+
121+
return json.Marshal(objT.Object)
122+
}

test/e2e/framework/test_context.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,14 @@ var (
9999
// Test suite authors can use framework/viper to make all command line
100100
// parameters also configurable via a configuration file.
101101
type TestContextType struct {
102-
KubeConfig string
103-
KubeContext string
104-
KubeAPIContentType string
105-
KubeletRootDir string
106-
CertDir string
107-
Host string
108-
BearerToken string `datapolicy:"token"`
102+
KubeConfig string
103+
KubeContext string
104+
KubeAPIContentType string
105+
KubeletRootDir string
106+
KubeletConfigDropinDir string
107+
CertDir string
108+
Host string
109+
BearerToken string `datapolicy:"token"`
109110
// TODO: Deprecating this over time... instead just use gobindata_util.go , see #23987.
110111
RepoRoot string
111112
// ListImages will list off all images that are used then quit

test/e2e/nodefeature/nodefeature.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ var (
3737
GracefulNodeShutdownBasedOnPodPriority = framework.WithNodeFeature(framework.ValidNodeFeatures.Add("GracefulNodeShutdownBasedOnPodPriority"))
3838
HostAccess = framework.WithNodeFeature(framework.ValidNodeFeatures.Add("HostAccess"))
3939
ImageID = framework.WithNodeFeature(framework.ValidNodeFeatures.Add(" ImageID"))
40+
KubeletConfigDropInDir = framework.WithNodeFeature(framework.ValidNodeFeatures.Add("KubeletConfigDropInDir"))
4041
LSCIQuotaMonitoring = framework.WithNodeFeature(framework.ValidNodeFeatures.Add("LSCIQuotaMonitoring"))
4142
NodeAllocatable = framework.WithNodeFeature(framework.ValidNodeFeatures.Add("NodeAllocatable"))
4243
NodeProblemDetector = framework.WithNodeFeature(framework.ValidNodeFeatures.Add("NodeProblemDetector"))

test/e2e_node/e2e_node_suite_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ func registerNodeFlags(flags *flag.FlagSet) {
9090
framework.TestContext.NodeE2E = true
9191
flags.StringVar(&framework.TestContext.BearerToken, "bearer-token", "", "The bearer token to authenticate with. If not specified, it would be a random token. Currently this token is only used in node e2e tests.")
9292
flags.StringVar(&framework.TestContext.NodeName, "node-name", "", "Name of the node to run tests on.")
93+
flags.StringVar(&framework.TestContext.KubeletConfigDropinDir, "config-dir", "", "Path to a directory containing drop-in configurations for the kubelet.")
9394
// TODO(random-liu): Move kubelet start logic out of the test.
9495
// TODO(random-liu): Move log fetch logic out of the test.
9596
// There are different ways to start kubelet (systemd, initd, docker, manually started etc.)
@@ -200,6 +201,14 @@ func TestE2eNode(t *testing.T) {
200201

201202
// We're not running in a special mode so lets run tests.
202203
gomega.RegisterFailHandler(ginkgo.Fail)
204+
// Initialize the KubeletConfigDropinDir again if the test doesn't run in run-kubelet-mode.
205+
if framework.TestContext.KubeletConfigDropinDir == "" {
206+
var err error
207+
framework.TestContext.KubeletConfigDropinDir, err = services.KubeletConfigDirCWDDir()
208+
if err != nil {
209+
klog.Errorf("failed to create kubelet config directory: %v", err)
210+
}
211+
}
203212
reportDir := framework.TestContext.ReportDir
204213
if reportDir != "" {
205214
// Create the directory if it doesn't already exist

0 commit comments

Comments
 (0)