Skip to content

Commit 6406fee

Browse files
committed
fix(lifecycle): support runtime kubeadm config helper
1 parent 3de7aa5 commit 6406fee

8 files changed

Lines changed: 437 additions & 67 deletions

File tree

lifecycle/pkg/runtime/kubernetes/config_versioning.go

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,48 @@ func shouldUseKubeadmV1beta4Features(version string) (bool, error) {
3939
}
4040

4141
func marshalConfigsForVersion(version string, configs ...interface{}) ([]byte, error) {
42-
enableV1beta4Features, err := shouldUseKubeadmV1beta4Features(version)
42+
enableV1beta4Features, preV130, err := configVersionFeatures(version)
4343
if err != nil {
4444
return nil, err
4545
}
4646

47+
return marshalConfigs(version, enableV1beta4Features, preV130, configs...)
48+
}
49+
50+
func marshalRawConfigsForVersion(version string, configs ...interface{}) ([]byte, error) {
51+
enableV1beta4Features, _, err := configVersionFeatures(version)
52+
if err != nil {
53+
return nil, err
54+
}
55+
56+
return marshalConfigs(version, enableV1beta4Features, false, configs...)
57+
}
58+
59+
func configVersionFeatures(version string) (enableV1beta4Features, preV130 bool, err error) {
60+
enableV1beta4Features, err = shouldUseKubeadmV1beta4Features(version)
61+
if err != nil {
62+
return false, false, err
63+
}
64+
preV130, err = isPreKubernetesV130(version)
65+
if err != nil {
66+
return false, false, err
67+
}
68+
return enableV1beta4Features, preV130, nil
69+
}
70+
71+
func marshalConfigs(_ string, enableV1beta4Features, preV130 bool, configs ...interface{}) ([]byte, error) {
4772
docs := make([][]byte, 0, len(configs))
4873
for _, cfg := range configs {
4974
data, err := yaml.Marshal(cfg)
5075
if err != nil {
5176
return nil, err
5277
}
78+
if preV130 {
79+
data, err = sanitizeConfigForPreV130(data)
80+
if err != nil {
81+
return nil, err
82+
}
83+
}
5384
if enableV1beta4Features {
5485
data, err = appendKubeadmCertValidityPeriods(data)
5586
if err != nil {
@@ -89,3 +120,71 @@ func appendKubeadmCertValidityPeriods(raw []byte) ([]byte, error) {
89120
}
90121
return append(raw, []byte(strings.Join(additions, "\n")+"\n")...), nil
91122
}
123+
124+
func marshalKubeletConfigForVersion(config interface{}, version string) ([]byte, error) {
125+
kubeletConfig, err := yaml.MarshalConfigs(config)
126+
if err != nil {
127+
return nil, err
128+
}
129+
return sanitizeKubeletConfigForVersion(kubeletConfig, version)
130+
}
131+
132+
func sanitizeKubeletConfigForVersion(kubeletConfig []byte, version string) ([]byte, error) {
133+
preV130, err := isPreKubernetesV130(version)
134+
if err != nil {
135+
return nil, err
136+
}
137+
if !preV130 {
138+
return kubeletConfig, nil
139+
}
140+
return sanitizeConfigForPreV130(kubeletConfig)
141+
}
142+
143+
func isPreKubernetesV130(version string) (bool, error) {
144+
sver, err := semver.NewVersion(version)
145+
if err != nil {
146+
return false, err
147+
}
148+
return sver.LessThan(V1300), nil
149+
}
150+
151+
func sanitizeConfigForPreV130(raw []byte) ([]byte, error) {
152+
config, err := yaml.UnmarshalToMap(raw)
153+
if err != nil {
154+
return nil, err
155+
}
156+
157+
kind, _ := config["kind"].(string)
158+
switch kind {
159+
case "KubeletConfiguration":
160+
delete(config, "containerLogMaxWorkers")
161+
delete(config, "containerLogMonitorInterval")
162+
delete(config, "containerRuntimeEndpoint")
163+
delete(config, "imageMaximumGCAge")
164+
delete(config, "podLogsDir")
165+
deleteNestedMapKey(config, "logging", "options", "text")
166+
case "KubeProxyConfiguration":
167+
delete(config, "logging")
168+
delete(config, "nftables")
169+
deleteNestedMapKey(config, "conntrack", "tcpBeLiberal")
170+
deleteNestedMapKey(config, "conntrack", "udpStreamTimeout")
171+
deleteNestedMapKey(config, "conntrack", "udpTimeout")
172+
}
173+
174+
return yaml.Marshal(config)
175+
}
176+
177+
func deleteNestedMapKey(data map[string]interface{}, keys ...string) {
178+
if len(keys) == 0 {
179+
return
180+
}
181+
current := data
182+
for _, key := range keys[:len(keys)-1] {
183+
next, ok := current[key].(map[string]interface{})
184+
if !ok {
185+
return
186+
}
187+
current = next
188+
}
189+
delete(current, keys[len(keys)-1])
190+
}

lifecycle/pkg/runtime/kubernetes/kubeadm.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,7 @@ func (k *KubeadmRuntime) generateInitConfigs() ([]byte, error) {
479479
if err != nil {
480480
return nil, err
481481
}
482-
return marshalConfigsForVersion(k.kubeadmConfig.ClusterConfiguration.KubernetesVersion,
482+
return k.marshalKubeadmConfigs(
483483
&conversion.InitConfiguration,
484484
&conversion.ClusterConfiguration,
485485
&conversion.KubeletConfiguration,
@@ -525,7 +525,7 @@ func (k *KubeadmRuntime) generateJoinNodeConfigs(node string) ([]byte, error) {
525525
if err != nil {
526526
return nil, err
527527
}
528-
return yaml.MarshalConfigs(
528+
return k.marshalKubeadmConfigs(
529529
&conversion.KubeletConfiguration,
530530
&conversion.JoinConfiguration)
531531
}
@@ -545,7 +545,9 @@ func (k *KubeadmRuntime) generateJoinMasterConfigs(masterIP string) ([]byte, err
545545
if err != nil {
546546
return nil, err
547547
}
548-
return yaml.MarshalConfigs(&conversion.JoinConfiguration, &conversion.KubeletConfiguration)
548+
return k.marshalKubeadmConfigs(
549+
&conversion.JoinConfiguration,
550+
&conversion.KubeletConfiguration)
549551
}
550552

551553
func (k *KubeadmRuntime) setCGroupDriverAndSocket(node string) error {
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
// Copyright © 2026 sealos.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package kubernetes
16+
17+
import (
18+
"bytes"
19+
"context"
20+
"errors"
21+
"fmt"
22+
"os"
23+
"os/exec"
24+
"path/filepath"
25+
"strings"
26+
"time"
27+
28+
"github.com/labring/sealos/pkg/utils/logger"
29+
)
30+
31+
const (
32+
kubeadmConfigHelperLabel = "sealos.io.kubeadm-config-helper"
33+
kubeadmConfigHelperAPILabel = "sealos.io.kubeadm-config-helper-api"
34+
kubeadmConfigHelperModeLabel = "sealos.io.kubeadm-config-helper-mode"
35+
36+
kubeadmConfigHelperAPIV1 = "v1"
37+
kubeadmConfigHelperModeSanitize = "sanitize"
38+
kubeadmConfigHelperTimeout = 10 * time.Second
39+
kubeadmConfigHelperMaxOutput = 4 << 20
40+
)
41+
42+
func (k *KubeadmRuntime) marshalKubeadmConfigs(configs ...interface{}) ([]byte, error) {
43+
version := k.kubeadmConfig.ClusterConfiguration.KubernetesVersion
44+
raw, err := marshalRawConfigsForVersion(version, configs...)
45+
if err != nil {
46+
return nil, err
47+
}
48+
49+
if out, ok, err := k.runKubeadmConfigHelper(raw, version); err != nil {
50+
return nil, err
51+
} else if ok {
52+
return out, nil
53+
}
54+
55+
return sanitizeRenderedConfigsForVersion(raw, version)
56+
}
57+
58+
func sanitizeRenderedConfigsForVersion(raw []byte, version string) ([]byte, error) {
59+
preV130, err := isPreKubernetesV130(version)
60+
if err != nil {
61+
return nil, err
62+
}
63+
if !preV130 {
64+
return raw, nil
65+
}
66+
67+
docs := bytes.Split(raw, []byte("\n---\n"))
68+
for i, doc := range docs {
69+
trimmed := bytes.TrimSpace(doc)
70+
if len(trimmed) == 0 {
71+
continue
72+
}
73+
sanitized, err := sanitizeConfigForPreV130(doc)
74+
if err != nil {
75+
return nil, err
76+
}
77+
docs[i] = sanitized
78+
}
79+
return bytes.Join(docs, []byte("\n---\n")), nil
80+
}
81+
82+
func (k *KubeadmRuntime) runKubeadmConfigHelper(raw []byte, version string) ([]byte, bool, error) {
83+
helper, apiVersion, mode, ok := k.kubeadmConfigHelper()
84+
if !ok {
85+
return nil, false, nil
86+
}
87+
if apiVersion != kubeadmConfigHelperAPIV1 {
88+
return nil, true, fmt.Errorf("unsupported kubeadm config helper API %q", apiVersion)
89+
}
90+
if mode != kubeadmConfigHelperModeSanitize {
91+
return nil, true, fmt.Errorf("unsupported kubeadm config helper mode %q", mode)
92+
}
93+
if !isExecutable(helper) {
94+
return nil, true, fmt.Errorf("kubeadm config helper %s is not executable", helper)
95+
}
96+
97+
ctx, cancel := context.WithTimeout(context.Background(), kubeadmConfigHelperTimeout)
98+
defer cancel()
99+
100+
cmd := exec.CommandContext(ctx, helper,
101+
"sanitize",
102+
"--kubernetes-version", version,
103+
"--helper-api", apiVersion,
104+
)
105+
cmd.Stdin = bytes.NewReader(raw)
106+
var stdout, stderr limitedBuffer
107+
stdout.limit = kubeadmConfigHelperMaxOutput
108+
stderr.limit = 64 << 10
109+
cmd.Stdout = &stdout
110+
cmd.Stderr = &stderr
111+
112+
if err := cmd.Run(); err != nil {
113+
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
114+
return nil, true, fmt.Errorf("kubeadm config helper timed out")
115+
}
116+
return nil, true, fmt.Errorf("kubeadm config helper failed: %w: %s", err, strings.TrimSpace(stderr.String()))
117+
}
118+
if stdout.truncated {
119+
return nil, true, fmt.Errorf("kubeadm config helper output exceeded %d bytes", kubeadmConfigHelperMaxOutput)
120+
}
121+
if stdout.Len() == 0 {
122+
return nil, true, fmt.Errorf("kubeadm config helper produced empty output")
123+
}
124+
125+
logger.Debug("kubeadm config helper %s sanitized config for %s", helper, version)
126+
return stdout.Bytes(), true, nil
127+
}
128+
129+
func (k *KubeadmRuntime) kubeadmConfigHelper() (helper, apiVersion, mode string, ok bool) {
130+
img := k.cluster.GetRootfsImage()
131+
if img == nil || img.Labels == nil {
132+
return "", "", "", false
133+
}
134+
135+
helper = img.Labels[kubeadmConfigHelperLabel]
136+
if helper == "" {
137+
return "", "", "", false
138+
}
139+
if !filepath.IsAbs(helper) {
140+
return "", "", "", false
141+
}
142+
apiVersion = img.Labels[kubeadmConfigHelperAPILabel]
143+
if apiVersion == "" {
144+
apiVersion = kubeadmConfigHelperAPIV1
145+
}
146+
mode = img.Labels[kubeadmConfigHelperModeLabel]
147+
if mode == "" {
148+
mode = kubeadmConfigHelperModeSanitize
149+
}
150+
151+
rootfsPath := img.MountPoint
152+
if rootfsPath == "" {
153+
rootfsPath = k.pathResolver.RootFSPath()
154+
}
155+
rootfsPath, err := filepath.EvalSymlinks(rootfsPath)
156+
if err != nil {
157+
return "", "", "", false
158+
}
159+
candidate := filepath.Join(rootfsPath, strings.TrimPrefix(filepath.Clean(helper), string(filepath.Separator)))
160+
candidate, err = filepath.EvalSymlinks(candidate)
161+
if err != nil {
162+
return "", "", "", false
163+
}
164+
if candidate != rootfsPath && !strings.HasPrefix(candidate, rootfsPath+string(filepath.Separator)) {
165+
return "", "", "", false
166+
}
167+
return candidate, apiVersion, mode, true
168+
}
169+
170+
func isExecutable(path string) bool {
171+
info, err := os.Stat(path)
172+
return err == nil && !info.IsDir() && info.Mode()&0111 != 0
173+
}
174+
175+
type limitedBuffer struct {
176+
bytes.Buffer
177+
limit int
178+
truncated bool
179+
}
180+
181+
func (b *limitedBuffer) Write(p []byte) (int, error) {
182+
if b.limit <= 0 || b.Len()+len(p) <= b.limit {
183+
return b.Buffer.Write(p)
184+
}
185+
remaining := b.limit - b.Len()
186+
if remaining > 0 {
187+
_, _ = b.Buffer.Write(p[:remaining])
188+
}
189+
b.truncated = true
190+
return len(p), nil
191+
}

lifecycle/pkg/runtime/kubernetes/runtime.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func (k *KubeadmRuntime) GetRawConfig() ([]byte, error) {
8181
conversion.KubeProxyConfiguration,
8282
conversion.KubeletConfiguration,
8383
}
84-
data, err := marshalConfigsForVersion(k.kubeadmConfig.ClusterConfiguration.KubernetesVersion, objects...)
84+
data, err := k.marshalKubeadmConfigs(objects...)
8585
if err != nil {
8686
return nil, err
8787
}

0 commit comments

Comments
 (0)