diff --git a/Makefile b/Makefile index 61a4409df..fdd046eba 100644 --- a/Makefile +++ b/Makefile @@ -97,6 +97,8 @@ kmesh-bpf: $(QUIET) make -C bpf/deserialization_to_bpf_map $(QUIET) $(GO) generate bpf/kmesh/bpf2go/bpf2go.go + + $(QUIET) $(GO) run hack/gen_bpf_specs.go kmesh-ko: $(QUIET) find $(ROOT_DIR)/mk -name "*.pc" | xargs sed -i "s#^prefix=.*#prefix=${ROOT_DIR}#g" $(call printlog, BUILD, "kernel") diff --git a/hack/gen_bpf2go_exec.sh b/hack/gen_bpf2go_exec.sh index a2dfd7790..90af0e58e 100755 --- a/hack/gen_bpf2go_exec.sh +++ b/hack/gen_bpf2go_exec.sh @@ -8,5 +8,6 @@ kmesh_exec() { set_enhanced_kernel_env prepare go generate bpf/kmesh/bpf2go/bpf2go.go + go run hack/gen_bpf_specs.go } kmesh_exec diff --git a/hack/gen_bpf_specs.go b/hack/gen_bpf_specs.go new file mode 100644 index 000000000..ff7a84040 --- /dev/null +++ b/hack/gen_bpf_specs.go @@ -0,0 +1,485 @@ +/* + * Copyright The Kmesh Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package main + +import ( + "bytes" + "fmt" + "go/format" + "os" + "path/filepath" + "regexp" + "sort" + "strings" + "text/template" +) + +type entry struct { + OutputDir string // e.g. kernelnative/normal or kernelnative/enhanced or dualengine or general + GoPkg string // go package name passed in --go-package + Symbol string // the symbol token (KmeshCgroupSock etc) +} + +type pkgInfo struct { + Alias string + ImportPath string + OutputDir string + Entries []entry +} + +type symbolGen struct { + BaseName string // e.g. KmeshSockops + NormalPkgAlias string // alias that provides normal symbol (may be "") + NormalSymbol string // e.g. KmeshSockops + CompatPkgAlias string // alias that provides compat symbol (may be "") + CompatSymbol string // e.g. KmeshSockopsCompat +} + +func main() { + root, err := repoRoot() + if err != nil { + fmt.Fprintf(os.Stderr, "failed detect repo root: %v\n", err) + os.Exit(2) + } + + files := collectGoFiles(root) + + // regexes + genRe := regexp.MustCompile(`//\s*go:generate\s+(?:.*\b)bpf2go\b(.*)`) + outDirRe := regexp.MustCompile(`--output-dir\s+([^\s]+)`) + goPkgRe := regexp.MustCompile(`--go-package\s+([^\s]+)`) + symbolRe := regexp.MustCompile(`\b([A-Za-z0-9_]+)\s+[^\s]+\.(?:c|C)\b`) + + var entries []entry + for _, f := range files { + b, _ := os.ReadFile(f) + for _, line := range strings.Split(string(b), "\n") { + m := genRe.FindStringSubmatch(line) + if m == nil { + continue + } + rest := m[1] + out := "" + if mm := outDirRe.FindStringSubmatch(rest); mm != nil { + out = mm[1] + } + pkg := "" + if mm := goPkgRe.FindStringSubmatch(rest); mm != nil { + pkg = mm[1] + } + if mm := symbolRe.FindStringSubmatch(rest); mm != nil { + sym := mm[1] + entries = append(entries, entry{OutputDir: out, GoPkg: pkg, Symbol: sym}) + } + } + } + + if len(entries) == 0 { + fmt.Println("no bpf2go generate lines found, nothing to do") + return + } + + byDir := map[string][]entry{} + for _, e := range entries { + dir := strings.Trim(e.OutputDir, `"'`) + byDir[dir] = append(byDir[dir], e) + } + + keys := make([]string, 0, len(byDir)) + for k := range byDir { + keys = append(keys, k) + } + sort.Strings(keys) + + modulePrefix := detectModulePath(root) + if modulePrefix == "" { + modulePrefix = "kmesh.net/kmesh" + } + + sanitizeRe := regexp.MustCompile(`[^A-Za-z0-9_]`) + sanitize := func(s string) string { + out := sanitizeRe.ReplaceAllString(s, "_") + out = strings.Trim(out, "_") + if out == "" { + out = "pkg" + } + if out[0] >= '0' && out[0] <= '9' { + out = "pkg_" + out + } + return out + } + + baseBpf2go := filepath.ToSlash(filepath.Join(root, "bpf", "kmesh", "bpf2go")) + + var pkgsDefault []pkgInfo + var pkgsEnhanced []pkgInfo + + for _, k := range keys { + list := byDir[k] + pattern := strings.Trim(k, `"'`) + + var realDirs []string + if strings.ContainsAny(pattern, "*?[]") { + globFull := filepath.ToSlash(filepath.Join(baseBpf2go, pattern)) + matches, _ := filepath.Glob(globFull) + for _, m := range matches { + if fi, err := os.Stat(m); err == nil && fi.IsDir() { + rel, err := filepath.Rel(baseBpf2go, m) + if err == nil { + realDirs = append(realDirs, filepath.ToSlash(rel)) + } + } + } + if len(realDirs) == 0 { + realDirs = append(realDirs, pattern) + } + } else { + realDirs = append(realDirs, pattern) + } + + for _, real := range realDirs { + if strings.Contains(real, "$ENHANCED_KERNEL") { + rdDefault := strings.ReplaceAll(real, "$ENHANCED_KERNEL", "normal") + aliasDef := sanitize(rdDefault) + importDef := filepath.ToSlash(filepath.Join(modulePrefix, "bpf", "kmesh", "bpf2go", rdDefault)) + pkgsDefault = append(pkgsDefault, pkgInfo{Alias: aliasDef, ImportPath: importDef, OutputDir: rdDefault, Entries: list}) + + rdEnh := strings.ReplaceAll(real, "$ENHANCED_KERNEL", "enhanced") + aliasEnh := sanitize(rdEnh) + importEnh := filepath.ToSlash(filepath.Join(modulePrefix, "bpf", "kmesh", "bpf2go", rdEnh)) + pkgsEnhanced = append(pkgsEnhanced, pkgInfo{Alias: aliasEnh, ImportPath: importEnh, OutputDir: rdEnh, Entries: list}) + } else { + alias := sanitize(real) + importPath := filepath.ToSlash(filepath.Join(modulePrefix, "bpf", "kmesh", "bpf2go", real)) + pi := pkgInfo{Alias: alias, ImportPath: importPath, OutputDir: real, Entries: list} + pkgsDefault = append(pkgsDefault, pi) + pkgsEnhanced = append(pkgsEnhanced, pi) + } + } + } + + sort.Slice(pkgsDefault, func(i, j int) bool { return pkgsDefault[i].ImportPath < pkgsDefault[j].ImportPath }) + sort.Slice(pkgsEnhanced, func(i, j int) bool { return pkgsEnhanced[i].ImportPath < pkgsEnhanced[j].ImportPath }) + + // build per-bucket symbol gens + knDefault := filterPkgsByPrefix(pkgsDefault, "kernelnative") + deDefault := filterPkgsByPrefix(pkgsDefault, "dualengine") + genDefault := filterPkgsByPrefix(pkgsDefault, "general") + + knEnhanced := filterPkgsByPrefix(pkgsEnhanced, "kernelnative") + + symsKnDefault := buildSymbolGen(knDefault) + symsDeDefault := buildSymbolGen(deDefault) + symsGenDefault := buildSymbolGen(genDefault) + + symsKnEnhanced := buildSymbolGen(knEnhanced) + + // prepare output dir + outDir := filepath.Join(root, "pkg", "bpf", "restart") + if err := os.MkdirAll(outDir, 0755); err != nil { + fmt.Fprintf(os.Stderr, "mkdir target dir: %v\n", err) + os.Exit(2) + } + + // template + tplText := `/* + * Copyright The Kmesh Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Code generated by hack/gen_bpf_specs.go; DO NOT EDIT. + +package restart + +import ( + "fmt" + + "github.com/cilium/ebpf" + + "kmesh.net/kmesh/daemon/options" + helper "kmesh.net/kmesh/pkg/utils" +{{- range .Pkgs }} + {{ .Alias }} "{{ .ImportPath }}" +{{- end }} +) + +// Auto-generated: keeps in sync with //go:generate bpf2go lines. +func LoadCompileTimeSpecs(config *options.BpfConfig) (map[string]map[string]*ebpf.MapSpec, error) { + specs := make(map[string]map[string]*ebpf.MapSpec) + + if config.KernelNativeEnabled() { +{{- range .SymsKn }} +{{- if and .NormalPkgAlias .CompatPkgAlias }} + // Symbol {{ .BaseName }} has both normal and compat variants. + if helper.KernelVersionLowerThan5_13() { + if coll, err := {{ .CompatPkgAlias }}.Load{{ .CompatSymbol }}(); err != nil { + return nil, fmt.Errorf("load Compat {{ .CompatSymbol }} spec: %w", err) + } else { + specs["{{ .BaseName }}Compat"] = coll.Maps + } + } else { + if coll, err := {{ .NormalPkgAlias }}.Load{{ .NormalSymbol }}(); err != nil { + return nil, fmt.Errorf("load {{ .NormalSymbol }} spec: %w", err) + } else { + specs["{{ .BaseName }}"] = coll.Maps + } + } +{{- else if .NormalPkgAlias }} + // Symbol {{ .BaseName }} only normal + if coll, err := {{ .NormalPkgAlias }}.Load{{ .NormalSymbol }}(); err != nil { + return nil, fmt.Errorf("load {{ .NormalSymbol }} spec: %w", err) + } else { + specs["{{ .BaseName }}"] = coll.Maps + } +{{- else if .CompatPkgAlias }} + // Symbol {{ .BaseName }} only compat + if coll, err := {{ .CompatPkgAlias }}.Load{{ .CompatSymbol }}(); err != nil { + return nil, fmt.Errorf("load Compat {{ .CompatSymbol }} spec: %w", err) + } else { + specs["{{ .BaseName }}Compat"] = coll.Maps + } +{{- end }} + +{{- end }} + } else if config.DualEngineEnabled() { +{{- range .SymsDe }} +{{- if and .NormalPkgAlias .CompatPkgAlias }} + // Symbol {{ .BaseName }} has both normal and compat variants (dualengine). + if helper.KernelVersionLowerThan5_13() { + if coll, err := {{ .CompatPkgAlias }}.Load{{ .CompatSymbol }}(); err != nil { + return nil, fmt.Errorf("load Compat {{ .CompatSymbol }} spec: %w", err) + } else { + specs["{{ .BaseName }}Compat"] = coll.Maps + } + } else { + if coll, err := {{ .NormalPkgAlias }}.Load{{ .NormalSymbol }}(); err != nil { + return nil, fmt.Errorf("load {{ .NormalSymbol }} spec: %w", err) + } else { + specs["{{ .BaseName }}"] = coll.Maps + } + } +{{- else if .NormalPkgAlias }} + // Symbol {{ .BaseName }} only normal (dualengine) + if coll, err := {{ .NormalPkgAlias }}.Load{{ .NormalSymbol }}(); err != nil { + return nil, fmt.Errorf("load {{ .NormalSymbol }} spec: %w", err) + } else { + specs["{{ .BaseName }}"] = coll.Maps + } +{{- else if .CompatPkgAlias }} + // Symbol {{ .BaseName }} only compat (dualengine) + if coll, err := {{ .CompatPkgAlias }}.Load{{ .CompatSymbol }}(); err != nil { + return nil, fmt.Errorf("load Compat {{ .CompatSymbol }} spec: %w", err) + } else { + specs["{{ .BaseName }}Compat"] = coll.Maps + } +{{- end }} +{{- end }} + } + +{{- range .SymsGen }} +{{- if and .NormalPkgAlias .CompatPkgAlias }} + // General Symbol {{ .BaseName }} has normal+compat (choose by kernel) + if helper.KernelVersionLowerThan5_13() { + if coll, err := {{ .CompatPkgAlias }}.Load{{ .CompatSymbol }}(); err != nil { + return nil, fmt.Errorf("load Compat {{ .CompatSymbol }} spec: %w", err) + } else { + specs["{{ .BaseName }}Compat"] = coll.Maps + } + } else { + if coll, err := {{ .NormalPkgAlias }}.Load{{ .NormalSymbol }}(); err != nil { + return nil, fmt.Errorf("load {{ .NormalSymbol }} spec: %w", err) + } else { + specs["{{ .BaseName }}"] = coll.Maps + } + } +{{- else if .NormalPkgAlias }} + if coll, err := {{ .NormalPkgAlias }}.Load{{ .NormalSymbol }}(); err != nil { + return nil, fmt.Errorf("load General {{ .NormalSymbol }} spec: %w", err) + } else { + specs["{{ .BaseName }}"] = coll.Maps + } +{{- else if .CompatPkgAlias }} + if coll, err := {{ .CompatPkgAlias }}.Load{{ .CompatSymbol }}(); err != nil { + return nil, fmt.Errorf("load General Compat {{ .CompatSymbol }} spec: %w", err) + } else { + specs["{{ .BaseName }}Compat"] = coll.Maps + } +{{- end }} +{{- end }} + + return specs, nil +} +` + + funcMap := template.FuncMap{} + tpl, err := template.New("out").Funcs(funcMap).Parse(tplText) + if err != nil { + fmt.Fprintf(os.Stderr, "parse tpl: %v\n", err) + os.Exit(2) + } + + // render default (non-enhanced) file + if err := renderVariant(tpl, pkgsDefault, symsKnDefault, symsDeDefault, symsGenDefault, filepath.Join(outDir, "new_version_mapspec_loader.go"), "!enhanced"); err != nil { + fmt.Fprintf(os.Stderr, "write default: %v\n", err) + os.Exit(2) + } + + // render enhanced file + if err := renderVariant(tpl, pkgsEnhanced, symsKnEnhanced, []symbolGen{}, symsGenDefault, filepath.Join(outDir, "new_version_mapspec_loader_enhanced.go"), "enhanced"); err != nil { + fmt.Fprintf(os.Stderr, "write enhanced: %v\n", err) + os.Exit(2) + } +} + +func collectGoFiles(root string) []string { + files := []string{} + _ = filepath.Walk(root, func(path string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + if fi.IsDir() { + if fi.Name() == "vendor" || strings.HasPrefix(path, filepath.Join(root, ".git")) { + return filepath.SkipDir + } + return nil + } + if strings.HasSuffix(path, ".go") { + files = append(files, path) + } + return nil + }) + return files +} + +func detectModulePath(root string) string { + modf := filepath.Join(root, "go.mod") + b, err := os.ReadFile(modf) + if err != nil { + return "" + } + for _, line := range strings.Split(string(b), "\n") { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "module ") { + return strings.TrimSpace(strings.TrimPrefix(line, "module ")) + } + } + return "" +} + +func repoRoot() (string, error) { + dir, err := os.Getwd() + if err != nil { + return "", err + } + return dir, nil +} + +func filterPkgsByPrefix(pkgs []pkgInfo, prefix string) []pkgInfo { + out := []pkgInfo{} + for _, p := range pkgs { + trim := strings.TrimPrefix(strings.TrimSpace(p.OutputDir), "/") + if strings.HasPrefix(trim, prefix) { + out = append(out, p) + } + } + sort.Slice(out, func(i, j int) bool { return out[i].ImportPath < out[j].ImportPath }) + return out +} + +func buildSymbolGen(pkgs []pkgInfo) []symbolGen { + m := map[string]*symbolGen{} + for _, p := range pkgs { + for _, e := range p.Entries { + sym := e.Symbol + base := sym + isCompat := false + if strings.HasSuffix(sym, "Compat") { + base = strings.TrimSuffix(sym, "Compat") + isCompat = true + } + if _, ok := m[base]; !ok { + m[base] = &symbolGen{BaseName: base} + } + sg := m[base] + if isCompat { + sg.CompatPkgAlias = p.Alias + sg.CompatSymbol = sym + } else { + sg.NormalPkgAlias = p.Alias + sg.NormalSymbol = sym + } + } + } + out := make([]symbolGen, 0, len(m)) + for _, v := range m { + out = append(out, *v) + } + sort.Slice(out, func(i, j int) bool { return out[i].BaseName < out[j].BaseName }) + return out +} + +func renderVariant(tpl *template.Template, pkgs []pkgInfo, symsKn, symsDe, symsGen []symbolGen, outPath, buildTag string) error { + // combine pkgs into template data (unique by import path) + uniq := map[string]pkgInfo{} + for _, p := range pkgs { + uniq[p.ImportPath] = p + } + list := make([]pkgInfo, 0, len(uniq)) + for _, v := range uniq { + list = append(list, v) + } + sort.Slice(list, func(i, j int) bool { return list[i].ImportPath < list[j].ImportPath }) + + buf := &bytes.Buffer{} + err := tpl.Execute(buf, map[string]interface{}{ + "Pkgs": list, + "SymsKn": symsKn, + "SymsDe": symsDe, + "SymsGen": symsGen, + }) + if err != nil { + return fmt.Errorf("execute tpl: %w", err) + } + + var out []byte + if buildTag != "" { + header := fmt.Sprintf("//go:build %s\n// +build %s\n\n", buildTag, buildTag) + out = append([]byte(header), buf.Bytes()...) + } else { + out = buf.Bytes() + } + + src, err := format.Source(out) + if err != nil { + return fmt.Errorf("gofmt failed: %w\nraw output:\n%s\n", err, string(out)) + } + if err := os.WriteFile(outPath, src, 0644); err != nil { + return fmt.Errorf("write out: %w", err) + } + return nil +} diff --git a/pkg/bpf/ads/sock_connection.go b/pkg/bpf/ads/sock_connection.go index 2f72f6ba2..abae90678 100644 --- a/pkg/bpf/ads/sock_connection.go +++ b/pkg/bpf/ads/sock_connection.go @@ -140,7 +140,7 @@ func (sc *BpfSockConn) Attach() error { } progPinPath := filepath.Join(sc.Info.BpfFsPath, constants.Prog_link) - if restart.GetStartType() == restart.Restart { + if restart.GetStartType() == restart.Restart || restart.GetStartType() == restart.Update { if sc.Link, err = utils.BpfProgUpdate(progPinPath, cgopt); err != nil { return err } diff --git a/pkg/bpf/ads/sock_ops.go b/pkg/bpf/ads/sock_ops.go index 579638f57..d6fa974b4 100644 --- a/pkg/bpf/ads/sock_ops.go +++ b/pkg/bpf/ads/sock_ops.go @@ -95,7 +95,7 @@ func (sc *BpfSockOps) Attach() error { // pin bpf_link progPinPath := filepath.Join(sc.Info.BpfFsPath, constants.Prog_link) - if restart.GetStartType() == restart.Restart { + if restart.GetStartType() == restart.Restart || restart.GetStartType() == restart.Update { if sc.Link, err = utils.BpfProgUpdate(progPinPath, cgopt); err != nil { return err } diff --git a/pkg/bpf/bpf.go b/pkg/bpf/bpf.go index 76d8b2e3b..b8ee93ac4 100644 --- a/pkg/bpf/bpf.go +++ b/pkg/bpf/bpf.go @@ -141,7 +141,7 @@ func StopMda() error { func (l *BpfLoader) Stop() { var err error C.deserial_uninit() - if restart.GetExitType() == restart.Restart { + if restart.GetExitType() == restart.Restart || restart.GetExitType() == restart.Update { return } @@ -194,8 +194,9 @@ func NewVersionMap(config *options.BpfConfig) *ebpf.Map { case restart.Restart: return versionMap case restart.Update: - // TODO : update mode has not been fully developed and is currently consistent with normal mode - log.Warnf("Update mode support is under development, Will be started in Normal mode.") + if updateVersionMap := restart.UpdateMapHandler(versionMap, versionPath, config); updateVersionMap != nil { + return updateVersionMap + } default: } @@ -231,9 +232,19 @@ func NewVersionMap(config *options.BpfConfig) *ebpf.Map { return nil } + mapspecs, err := restart.LoadCompileTimeSpecs(config) + if err != nil { + log.Errorf("failed to load compile time specs: %v", err) + return nil + } + storeVersionInfo(m) log.Infof("kmesh start with Normal") restart.SetStartType(restart.Normal) + if err := restart.SnapshotSpecsbyProg(mapspecs); err != nil { + log.Errorf("failed to store compile time specs: %v", err) + return nil + } return m } diff --git a/pkg/bpf/bpf_test.go b/pkg/bpf/bpf_test.go index eb46e4f63..0f78fe151 100644 --- a/pkg/bpf/bpf_test.go +++ b/pkg/bpf/bpf_test.go @@ -27,6 +27,9 @@ import ( "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/btf" + "kmesh.net/kmesh/daemon/options" "kmesh.net/kmesh/pkg/bpf/factory" "kmesh.net/kmesh/pkg/bpf/restart" @@ -246,3 +249,295 @@ func Test_getNodeIPAddress(t *testing.T) { }) } } + +// helper: build a simple btf.Int without relying on encoding constants +func intType(name string, sizeBytes int) *btf.Int { + return &btf.Int{ + Name: name, + Size: uint32(sizeBytes * 8), + } +} + +// Test diffStructInfoAgainstBTF basic cases: FieldAdded / FieldRemoved / Offset / Nested +func TestDiffStructInfoAgainstBTF_Basics(t *testing.T) { + // old StructInfo: one member "a" + old := restart.PersistedStructLayout{ + Name: "S_old", + Members: []restart.PersistedMemberLayout{ + { + Name: "a", + TypeName: "uint32", + Offset: 0, // we'll match against btf.Member.Offset below (no /8 used in current diff impl) + BitfieldSize: 0, + }, + }, + } + + // New BTF struct: has "a" and new field "b" => FieldAdded == true + newWithAdded := &btf.Struct{ + Name: "S_new_added", + Members: []btf.Member{ + { + Name: "a", + Type: intType("uint32", 4), + Offset: btf.Bits(0), + BitfieldSize: btf.Bits(32), + }, + { + Name: "b", + Type: intType("uint8", 1), + Offset: btf.Bits(32), + BitfieldSize: btf.Bits(0), + }, + }, + } + + d := restart.DiffStructInfoAgainstBTF(old, newWithAdded, make(map[string]bool)) + if !d.FieldAdded { + t.Fatalf("expected FieldAdded==true, got %+v", d) + } + + // New BTF struct: missing "a" => FieldRemoved true + newRemoved := &btf.Struct{ + Name: "S_new_removed", + Members: []btf.Member{ + { + Name: "x", + Type: intType("uint32", 4), + Offset: btf.Bits(0), + BitfieldSize: btf.Bits(0), + }, + }, + } + d = restart.DiffStructInfoAgainstBTF(old, newRemoved, make(map[string]bool)) + if !d.FieldRemoved { + t.Fatalf("expected FieldRemoved==true, got %+v", d) + } + + // Offset change: new has "a" but with different Offset + newOff := &btf.Struct{ + Name: "S_new_off", + Members: []btf.Member{ + { + Name: "a", + Type: intType("uint32", 4), + Offset: btf.Bits(8), // note: current diff code compares uint32(member.Offset) vs saved Offset + BitfieldSize: btf.Bits(0), + }, + }, + } + // To make the offset comparison hit, set old.Members[0].Offset to uint32(member.Offset) + oldOffsetMatch := old + oldOffsetMatch.Members[0].Offset = uint32(newOff.Members[0].Offset) // direct match -> no offset diff + d = restart.DiffStructInfoAgainstBTF(oldOffsetMatch, newOff, make(map[string]bool)) + if d.FieldOffsetChanged { + t.Fatalf("did not expect FieldOffsetChanged when offsets match (got %+v)", d) + } + // Now set old offset to different value -> expect FieldOffsetChanged + oldOffsetMismatch := old + oldOffsetMismatch.Members[0].Offset = uint32(0) + d = restart.DiffStructInfoAgainstBTF(oldOffsetMismatch, newOff, make(map[string]bool)) + if !d.FieldOffsetChanged { + t.Fatalf("expected FieldOffsetChanged true, got %+v", d) + } +} + +func TestDiffStructInfoAgainstBTF_NestedIncompatible(t *testing.T) { + // old: inner { a: uint32 }, outer { x: inner, y: uint64 } + innerInt := intType("uint32", 4) + oldInner := &btf.Struct{ + Name: "inner", + Members: []btf.Member{ + { + Name: "a", + Type: innerInt, + Offset: btf.Bits(0), + BitfieldSize: btf.Bits(0), + }, + }, + } + outerUint := intType("__u64", 8) + oldOuter := &btf.Struct{ + Name: "outer", + Members: []btf.Member{ + { + Name: "x", + Type: oldInner, + Offset: btf.Bits(0), + BitfieldSize: btf.Bits(0), + }, + { + Name: "y", + Type: outerUint, + Offset: btf.Bits(32), + BitfieldSize: btf.Bits(0), + }, + }, + } + + // persisted registry stores old definitions (StructInfo with Nested expanded) + registry := map[string]restart.PersistedStructLayout{ + "inner": { + Name: "inner", + Members: []restart.PersistedMemberLayout{ + { + Name: "a", + TypeName: "uint32", + Offset: uint32(oldInner.Members[0].Offset), // use raw bit value as in your diff impl + BitfieldSize: 0, + }, + }, + }, + "outer": { + Name: "outer", + Members: []restart.PersistedMemberLayout{ + { + Name: "x", + TypeName: "inner", + Offset: uint32(oldOuter.Members[0].Offset), + BitfieldSize: 0, + Nested: &restart.PersistedStructLayout{ + Name: "inner", + Members: []restart.PersistedMemberLayout{ + {Name: "a", TypeName: "uint32", Offset: uint32(oldInner.Members[0].Offset), BitfieldSize: 0}, + }, + }, + }, + { + Name: "y", + TypeName: "__u64", + Offset: uint32(oldOuter.Members[1].Offset), + BitfieldSize: 0, + }, + }, + }, + } + + // new: inner_changed { b: uint32 } <-- note: field name changed (a -> b) + newInnerChanged := &btf.Struct{ + Name: "inner_changed", + Members: []btf.Member{ + { + Name: "b", // different name -> incompatible + Type: intType("uint32", 4), + Offset: btf.Bits(0), + BitfieldSize: btf.Bits(0), + }, + }, + } + // new outer uses this new inner_changed type + newOuter := &btf.Struct{ + Name: "outer", + Members: []btf.Member{ + { + Name: "x", + Type: newInnerChanged, + Offset: btf.Bits(0), + BitfieldSize: btf.Bits(0), + }, + { + Name: "y", + Type: outerUint, + Offset: btf.Bits(32), + BitfieldSize: btf.Bits(0), + }, + }, + } + + // Compare persisted outer (StructInfo) against newOuter (btf.Struct). + diff := restart.DiffStructInfoAgainstBTF(registry["outer"], newOuter, make(map[string]bool)) + + // Expect nested change (because inner's member name changed a->b) + if !diff.NestedLayoutChanged && !diff.FieldTypeChanged && !diff.FieldRemoved && !diff.FieldAdded { + t.Fatalf("expected incompatibility detected (NestedLayoutChanged/FieldTypeChanged/FieldAdded/FieldRemoved), got %#v", diff) + } +} + +// Test nested struct comparisons and compatibility path in migrateMap +func TestDiffStructInfoAgainstBTF_NestedAndMigrateMap_Compatible(t *testing.T) { + // Build nested btf structs: inner { a:uint32 }, outer { x:inner, y:uint64 } + innerInt := intType("uint32", 4) + inner := &btf.Struct{ + Name: "inner", + Members: []btf.Member{ + { + Name: "a", + Type: innerInt, + Offset: btf.Bits(0), + BitfieldSize: btf.Bits(0), + }, + }, + } + outerUint := intType("__u64", 8) + outer := &btf.Struct{ + Name: "outer", + Members: []btf.Member{ + { + Name: "x", + Type: inner, + Offset: btf.Bits(0), + BitfieldSize: btf.Bits(0), + }, + { + Name: "y", + Type: outerUint, + Offset: btf.Bits(32), + BitfieldSize: btf.Bits(0), + }, + }, + } + + // persisted StructInfo registry: includes both inner and outer + registry := map[string]restart.PersistedStructLayout{ + "inner": { + Name: "inner", + Members: []restart.PersistedMemberLayout{ + {Name: "a", TypeName: "uint32", Offset: uint32(inner.Members[0].Offset), BitfieldSize: 0}, + }, + }, + "outer": { + Name: "outer", + Members: []restart.PersistedMemberLayout{ + {Name: "x", TypeName: "inner", Offset: uint32(outer.Members[0].Offset), BitfieldSize: 0, Nested: &restart.PersistedStructLayout{ + Name: "inner", + Members: []restart.PersistedMemberLayout{ + {Name: "a", TypeName: "uint32", Offset: uint32(inner.Members[0].Offset), BitfieldSize: 0}, + }, + }}, + {Name: "y", TypeName: "__u64", Offset: uint32(outer.Members[1].Offset), BitfieldSize: 0}, + }, + }, + } + + // persisted map spec using outer + oldMapSpec := restart.PersistedMapSpec{ + Name: "km_nested_map", + Type: ebpf.Hash.String(), + KeySize: 4, + ValueSize: 16, + MaxEntries: 128, + KeyStructInfo: restart.PersistedStructLayout{Name: "int", Members: nil}, + ValueStructInfo: registry["outer"], + } + + // new compiled MapSpec that uses the same outer struct + newMapSpec := &ebpf.MapSpec{ + Name: "km_nested_map", + Type: ebpf.Hash, + KeySize: 4, + ValueSize: 16, + MaxEntries: 128, + Key: intType("int", 4), // key type non-struct in this test + Value: outer, + } + + // Call migrateMap: because the persisted value layout matches newMapSpec.Value, + // migrateMap should consider them compatible and return (nil, nil) (no creation). + m, err := restart.MigrateMap(&oldMapSpec, newMapSpec, "pkg", "mapNested", filepath.Join(t.TempDir(), "mapping")) + if err != nil { + t.Fatalf("migrateMap returned unexpected error: %v", err) + } + if m != nil { + t.Fatalf("expected nil map (reuse existing), got non-nil: %v", m) + } +} diff --git a/pkg/bpf/restart/bpf_update.go b/pkg/bpf/restart/bpf_update.go new file mode 100644 index 000000000..19323fb4f --- /dev/null +++ b/pkg/bpf/restart/bpf_update.go @@ -0,0 +1,500 @@ +/* + * Copyright The Kmesh Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package restart + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "syscall" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/btf" + + "kmesh.net/kmesh/daemon/options" + "kmesh.net/kmesh/pkg/version" +) + +type StructFieldChanges struct { + FieldRemoved bool // A field was present in the old struct (A) but is now missing in the new struct (B). + FieldAdded bool // A new field was added to the struct. + FieldTypeChanged bool // A field with the same name has a different type. + FieldOffsetChanged bool // The memory offset of a field with the same name has changed. + NestedLayoutChanged bool // The layout of a nested struct has changed (e.g., its fields were added, removed, or changed). +} + +type PersistedMemberLayout struct { + Name string `json:"name"` + TypeName string `json:"typeName"` + Offset uint32 `json:"offset"` + BitfieldSize uint32 `json:"bitfieldsize"` // only have value when the type is bitfield + Nested *PersistedStructLayout `json:"nested,omitempty"` +} + +type PersistedStructLayout struct { + Name string `json:"name"` + Members []PersistedMemberLayout `json:"members"` +} + +type PersistedMapSpec struct { + Name string `json:"name"` + Type string `json:"type"` // MapType.String() + KeySize uint32 `json:"keySize"` + ValueSize uint32 `json:"valueSize"` + MaxEntries uint32 `json:"maxEntries"` + Flags uint32 `json:"flags"` + KeyStructInfo PersistedStructLayout `json:"keyInfo"` // get from btf.Struct + ValueStructInfo PersistedStructLayout `json:"valueInfo"` +} + +type PersistedSnapshot struct { + Maps map[string]map[string]PersistedMapSpec `json:"maps"` +} + +const ( + MapSpecDir = "/mnt/kmesh_mapspecs" + MapSpecFilename = "mapspecs_by_prog.json" +) + +// UpdateMapHandler handles the “Update” case in NewVersionMap. +// It will migrate any BPF maps whose on‑disk pin already exists but whose +// compiled MapSpec has changed +func UpdateMapHandler(versionMap *ebpf.Map, kmBpfPath string, config *options.BpfConfig) *ebpf.Map { + persistedSpecs, err := LoadPersistedSnapshot() + if err != nil { + log.Errorf("load persisted map spec failed") + return nil + } + if persistedSpecs == nil { + log.Errorf("persisted map spec is nil") + return nil + } + specsbyProg, err := LoadCompileTimeSpecs(config) + if err != nil { + log.Errorf("load oldSpecsbyProg failed") + return nil + } + progNames := unionKeys(specsbyProg, persistedSpecs.Maps) + for _, progName := range progNames { + newMaps := specsbyProg[progName] + oldMaps := persistedSpecs.Maps[progName] + if newMaps == nil || oldMaps == nil { + continue + } + + mapNames := unionKeys(newMaps, oldMaps) + for _, mapName := range mapNames { + newSpec, hasNew := newMaps[mapName] + oldSpec, hasOld := oldMaps[mapName] + + pinPath := filepath.Join(kmBpfPath, mapName) + + switch { + case !hasNew && hasOld: // clean up + oldMap, _ := ebpf.LoadPinnedMap(pinPath, &ebpf.LoadPinOptions{}) + if err := oldMap.Unpin(); err != nil && !os.IsNotExist(err) { + log.Warnf("failed to unpin old map: %v (continuing)", err) + } + if err := oldMap.Close(); err != nil { + log.Warnf("failed to close old map FD: %v (continuing)", err) + } + if err := os.Remove(pinPath); err != nil && !os.IsNotExist(err) { + log.Warnf("failed to remove old map pinpath: %v (continuing)", err) + } + case hasNew && !hasOld: + if _, err := createEmptyMap(newSpec, pinPath, mapName, nil); err != nil { + log.Errorf("create new map %s/%s failed: %v", progName, mapName, err) + } + case hasNew && hasOld: + if _, err := migrateMap(&oldSpec, newSpec, progName, mapName, pinPath); err != nil { + log.Errorf("migrate map %s/%s failed: %v", progName, mapName, err) + } + } + } + } + + log.Infof("kmesh start with Update") + updateVersionInfo(versionMap) + if err := SnapshotSpecsbyProg(specsbyProg); err != nil { + return versionMap + } + return versionMap +} + +func updateVersionInfo(versionMap *ebpf.Map) { + key := uint32(0) + var value uint32 + hash.Reset() + hash.Write([]byte(version.Get().GitVersion)) + value = hash.Sum32() + if err := versionMap.Put(&key, &value); err != nil { + log.Errorf("update Version Map failed, err is %v", err) + } +} + +func MigrateMap( + oldMapSpec *PersistedMapSpec, + newMapSpec *ebpf.MapSpec, + progName, mapName, pinPath string, +) (*ebpf.Map, error) { + return migrateMap(oldMapSpec, newMapSpec, progName, mapName, pinPath) +} + +func migrateMap( + oldMapSpec *PersistedMapSpec, + newMapSpec *ebpf.MapSpec, + progName, mapName, pinPath string, +) (*ebpf.Map, error) { + if oldMapSpec == nil { + return createEmptyMap(newMapSpec, pinPath, mapName, nil) + } + if oldMapSpec.Type != newMapSpec.Type.String() || + oldMapSpec.KeySize != newMapSpec.KeySize || + oldMapSpec.ValueSize != newMapSpec.ValueSize || + oldMapSpec.MaxEntries > newMapSpec.MaxEntries { + return createEmptyMapWithPinnedMap(newMapSpec, pinPath, mapName) + } + + if needsRecreate(oldMapSpec.KeyStructInfo, newMapSpec.Key) { + return createEmptyMapWithPinnedMap(newMapSpec, pinPath, mapName) + } + + if needsRecreate(oldMapSpec.ValueStructInfo, newMapSpec.Value) { + return createEmptyMapWithPinnedMap(newMapSpec, pinPath, mapName) + } + return nil, nil +} + +func needsRecreate(oldStruct PersistedStructLayout, newType btf.Type) bool { + if newType == nil { + if oldStruct.Name != "" || len(oldStruct.Members) != 0 { + return true + } + return false + } + + if newStruct, ok := newType.(*btf.Struct); ok { + diff := diffStructInfoAgainstBTF(oldStruct, newStruct, make(map[string]bool)) + if diff.FieldAdded || diff.FieldRemoved || diff.FieldTypeChanged || + diff.FieldOffsetChanged || diff.NestedLayoutChanged { + return true + } + return false + } + + newTypeName := newType.TypeName() + if len(oldStruct.Members) != 0 { + return true + } + if oldStruct.Name == "" { + return true + } + if oldStruct.Name != newTypeName { + return true + } + return false +} + +func createEmptyMapWithPinnedMap(spec *ebpf.MapSpec, pinPath, mapName string) (*ebpf.Map, error) { + oldMap, err := ebpf.LoadPinnedMap(pinPath, &ebpf.LoadPinOptions{}) + if err != nil { + return createEmptyMap(spec, pinPath, mapName, nil) + } + return createEmptyMap(spec, pinPath, mapName, oldMap) +} + +func DiffStructInfoAgainstBTF( + a PersistedStructLayout, + b *btf.Struct, + visited map[string]bool, +) StructFieldChanges { + return diffStructInfoAgainstBTF(a, b, visited) +} + +func diffStructInfoAgainstBTF( + a PersistedStructLayout, + b *btf.Struct, + visited map[string]bool, +) StructFieldChanges { + diff := StructFieldChanges{} + + oldMap := make(map[string]PersistedMemberLayout, len(a.Members)) + for _, m := range a.Members { + oldMap[m.Name] = m + } + newMap := make(map[string]btf.Member, len(b.Members)) + for _, m := range b.Members { + newMap[m.Name] = m + } + + // check added fields (present in new but not in old) + for name := range newMap { + if _, ok := oldMap[name]; !ok { + diff.FieldAdded = true + break + } + } + + // check removed / type / offset / nested changes + for name, map_old := range oldMap { + map_new, exists := newMap[name] + if !exists { + diff.FieldRemoved = true + break + } + + if map_old.Offset != uint32(map_new.Offset) || map_old.BitfieldSize != uint32(map_new.BitfieldSize) { + diff.FieldOffsetChanged = true + break + } + + if map_old.TypeName == map_new.Type.TypeName() { + if mbStruct, ok := map_new.Type.(*btf.Struct); ok { + if map_old.Nested != nil { + if map_old.Nested.Name != "" { + if !visited[map_old.Nested.Name] { + visited[map_old.Nested.Name] = true + nestedDiff := diffStructInfoAgainstBTF(*map_old.Nested, mbStruct, visited) + if nestedDiff.FieldAdded || nestedDiff.FieldRemoved || nestedDiff.FieldTypeChanged || + nestedDiff.FieldOffsetChanged || nestedDiff.NestedLayoutChanged { + diff.NestedLayoutChanged = true + break + } + } + continue + } + } + diff.NestedLayoutChanged = true + break + } else { // if new side is not struct + log.Info(map_old.Nested != nil) + if map_old.TypeName != map_new.Type.TypeName() || map_old.Nested != nil { + diff.FieldTypeChanged = true + break + } + } + } else { + diff.FieldTypeChanged = true + break + } + } + return diff +} + +func createEmptyMap(spec *ebpf.MapSpec, pinPath string, mapName string, oldMap *ebpf.Map) (*ebpf.Map, error) { + if oldMap == nil { + if err := os.Remove(pinPath); err != nil && !os.IsNotExist(err) { + return nil, fmt.Errorf("remove old pin %s failed: %w", pinPath, err) + } + m, err := ebpf.NewMap(spec) + if err != nil { + return nil, fmt.Errorf("new map %s: %w", mapName, err) + } + if err := os.MkdirAll(filepath.Dir(pinPath), syscall.S_IRUSR|syscall.S_IWUSR|syscall.S_IXUSR|syscall.S_IRGRP|syscall.S_IXGRP); err != nil && !os.IsExist(err) { + return nil, fmt.Errorf("mkdir %s: %w", filepath.Dir(pinPath), err) + } + if err := m.Pin(pinPath); err != nil { + return nil, fmt.Errorf("pin empty map %s: %w", pinPath, err) + } + return m, nil + } + + tmpPinPath := pinPath + ".tmp" + m, err := ebpf.NewMap(spec) + if err != nil { + return nil, fmt.Errorf("new map %s: %w", mapName, err) + } + if err := os.MkdirAll(filepath.Dir(tmpPinPath), 0755); err != nil && !os.IsExist(err) { + m.Close() + return nil, fmt.Errorf("mkdir %s: %w", tmpPinPath, err) + } + if err := m.Pin(tmpPinPath); err != nil { + m.Close() + return nil, fmt.Errorf("pin tmp map %s: %w", tmpPinPath, err) + } + if err := oldMap.Unpin(); err != nil && !os.IsNotExist(err) { + log.Warnf("failed to unpin old map %s: %v (continuing)", pinPath, err) + } + if err := oldMap.Close(); err != nil { + log.Warnf("failed to close old map FD: %v (continuing)", err) + } + if err := os.Remove(pinPath); err != nil && !os.IsNotExist(err) { + m.Close() + return nil, fmt.Errorf("remove old pin %s failed: %w", pinPath, err) + } + if err := os.Rename(tmpPinPath, pinPath); err != nil { + m.Close() + return nil, fmt.Errorf("rename tmp %s to %s failed: %w", tmpPinPath, pinPath, err) + } + return m, nil +} + +// This function correctly handles two maps with different value types. +func unionKeys[V1, V2 any](map1 map[string]V1, map2 map[string]V2) []string { + // Pre-allocate the set with a reasonable capacity. + set := make(map[string]struct{}, len(map1)) + + for k := range map1 { + set[k] = struct{}{} + } + + for k := range map2 { + set[k] = struct{}{} + } + + keys := make([]string, 0, len(set)) + for k := range set { + keys = append(keys, k) + } + return keys +} + +func buildStructInfoRecursive(t btf.Type, registry map[string]PersistedStructLayout, visited map[btf.Type]bool) PersistedStructLayout { + if t == nil { + return PersistedStructLayout{} + } + + st, ok := t.(*btf.Struct) + if !ok { + return PersistedStructLayout{Name: t.TypeName()} + } + + if existing, ok := registry[st.Name]; ok { + return existing + } + + if visited[t] { + return PersistedStructLayout{Name: st.Name} + } + + visited[t] = true + si := PersistedStructLayout{ + Name: st.Name, + Members: make([]PersistedMemberLayout, 0, len(st.Members)), + } + + for _, m := range st.Members { + offBytes := uint32(m.Offset) + sizeBytes := uint32(m.BitfieldSize) + log.Info("sizeBytes", sizeBytes) + mi := PersistedMemberLayout{ + Name: m.Name, + TypeName: m.Type.TypeName(), + Offset: offBytes, + BitfieldSize: sizeBytes, + Nested: nil, + } + + if nested, ok := m.Type.(*btf.Struct); ok { + nestedInfo := buildStructInfoRecursive(nested, registry, visited) + mi.Nested = &nestedInfo + registry[nestedInfo.Name] = nestedInfo + } + + si.Members = append(si.Members, mi) + } + registry[si.Name] = si + return si +} + +// SnapshotSpecsbyProg takes a nested map of BPF map specifications and persists them to a file. +// The structure of the input map `specsbyProg` is: +// +// map[programName] -> map[mapName] -> *ebpf.MapSpec +// +// - The first key (programName) is the name of the BPF program collection, +// e.g., "KmeshCgroupSock". +// - The second key (mapName) is the name of a specific BPF map within that program, +// e.g., "kmesh_endpoints". +func SnapshotSpecsbyProg(specsbyProg map[string]map[string]*ebpf.MapSpec) error { + wrapper := make(map[string]map[string]PersistedMapSpec, len(specsbyProg)) + registry := make(map[string]PersistedStructLayout) + visited := make(map[btf.Type]bool) + + for prog, maps := range specsbyProg { + wrapper[prog] = make(map[string]PersistedMapSpec, len(maps)) + for name, ms := range maps { + if ms == nil { + continue + } + + var keyInfo PersistedStructLayout + if ms.Key != nil { + keyInfo = buildStructInfoRecursive(ms.Key, registry, visited) + } else { + keyInfo = PersistedStructLayout{Name: ""} + } + + var valueInfo PersistedStructLayout + if ms.Value != nil { + valueInfo = buildStructInfoRecursive(ms.Value, registry, visited) + } else { + valueInfo = PersistedStructLayout{Name: ""} + } + + pms := PersistedMapSpec{ + Name: ms.Name, + Type: ms.Type.String(), + KeySize: ms.KeySize, + ValueSize: ms.ValueSize, + MaxEntries: ms.MaxEntries, + Flags: ms.Flags, + KeyStructInfo: keyInfo, + ValueStructInfo: valueInfo, + } + wrapper[prog][name] = pms + } + } + + snapshot := PersistedSnapshot{ + Maps: wrapper, + } + + if err := os.MkdirAll(MapSpecDir, 0755); err != nil { + return fmt.Errorf("mkdir specDir: %w", err) + } + data, err := json.MarshalIndent(snapshot, "", " ") + if err != nil { + return fmt.Errorf("marshal snapshot: %w", err) + } + tmp := filepath.Join(MapSpecDir, MapSpecFilename+".tmp") + target := filepath.Join(MapSpecDir, MapSpecFilename) + if err := os.WriteFile(tmp, data, 0644); err != nil { + return fmt.Errorf("write tmp snapshot: %w", err) + } + if err := os.Rename(tmp, target); err != nil { + return fmt.Errorf("rename wrapper: %w", err) + } + return nil +} + +func LoadPersistedSnapshot() (*PersistedSnapshot, error) { + path := filepath.Join(MapSpecDir, MapSpecFilename) + buf, err := os.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, fmt.Errorf("read snapshot file: %w", err) + } + var snap PersistedSnapshot + if err := json.Unmarshal(buf, &snap); err != nil { + return nil, fmt.Errorf("unmarshal snapshot: %w", err) + } + return &snap, nil +} diff --git a/pkg/bpf/restart/new_version_mapspec_loader.go b/pkg/bpf/restart/new_version_mapspec_loader.go new file mode 100644 index 000000000..7c9cb6f16 --- /dev/null +++ b/pkg/bpf/restart/new_version_mapspec_loader.go @@ -0,0 +1,171 @@ +//go:build !enhanced +// +build !enhanced + +/* + * Copyright The Kmesh Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Code generated by hack/gen_bpf_specs.go; DO NOT EDIT. + +package restart + +import ( + "fmt" + + "github.com/cilium/ebpf" + + dualengine "kmesh.net/kmesh/bpf/kmesh/bpf2go/dualengine" + general "kmesh.net/kmesh/bpf/kmesh/bpf2go/general" + kernelnative_normal "kmesh.net/kmesh/bpf/kmesh/bpf2go/kernelnative/normal" + "kmesh.net/kmesh/daemon/options" + helper "kmesh.net/kmesh/pkg/utils" +) + +// Auto-generated: keeps in sync with //go:generate bpf2go lines. +func LoadCompileTimeSpecs(config *options.BpfConfig) (map[string]map[string]*ebpf.MapSpec, error) { + specs := make(map[string]map[string]*ebpf.MapSpec) + + if config.KernelNativeEnabled() { + // Symbol KmeshCgroupSock has both normal and compat variants. + if helper.KernelVersionLowerThan5_13() { + if coll, err := kernelnative_normal.LoadKmeshCgroupSockCompat(); err != nil { + return nil, fmt.Errorf("load Compat KmeshCgroupSockCompat spec: %w", err) + } else { + specs["KmeshCgroupSockCompat"] = coll.Maps + } + } else { + if coll, err := kernelnative_normal.LoadKmeshCgroupSock(); err != nil { + return nil, fmt.Errorf("load KmeshCgroupSock spec: %w", err) + } else { + specs["KmeshCgroupSock"] = coll.Maps + } + } + // Symbol KmeshSockops has both normal and compat variants. + if helper.KernelVersionLowerThan5_13() { + if coll, err := kernelnative_normal.LoadKmeshSockopsCompat(); err != nil { + return nil, fmt.Errorf("load Compat KmeshSockopsCompat spec: %w", err) + } else { + specs["KmeshSockopsCompat"] = coll.Maps + } + } else { + if coll, err := kernelnative_normal.LoadKmeshSockops(); err != nil { + return nil, fmt.Errorf("load KmeshSockops spec: %w", err) + } else { + specs["KmeshSockops"] = coll.Maps + } + } + } else if config.DualEngineEnabled() { + // Symbol KmeshCgroupSkb has both normal and compat variants (dualengine). + if helper.KernelVersionLowerThan5_13() { + if coll, err := dualengine.LoadKmeshCgroupSkbCompat(); err != nil { + return nil, fmt.Errorf("load Compat KmeshCgroupSkbCompat spec: %w", err) + } else { + specs["KmeshCgroupSkbCompat"] = coll.Maps + } + } else { + if coll, err := dualengine.LoadKmeshCgroupSkb(); err != nil { + return nil, fmt.Errorf("load KmeshCgroupSkb spec: %w", err) + } else { + specs["KmeshCgroupSkb"] = coll.Maps + } + } + // Symbol KmeshCgroupSockWorkload has both normal and compat variants (dualengine). + if helper.KernelVersionLowerThan5_13() { + if coll, err := dualengine.LoadKmeshCgroupSockWorkloadCompat(); err != nil { + return nil, fmt.Errorf("load Compat KmeshCgroupSockWorkloadCompat spec: %w", err) + } else { + specs["KmeshCgroupSockWorkloadCompat"] = coll.Maps + } + } else { + if coll, err := dualengine.LoadKmeshCgroupSockWorkload(); err != nil { + return nil, fmt.Errorf("load KmeshCgroupSockWorkload spec: %w", err) + } else { + specs["KmeshCgroupSockWorkload"] = coll.Maps + } + } + // Symbol KmeshSendmsg has both normal and compat variants (dualengine). + if helper.KernelVersionLowerThan5_13() { + if coll, err := dualengine.LoadKmeshSendmsgCompat(); err != nil { + return nil, fmt.Errorf("load Compat KmeshSendmsgCompat spec: %w", err) + } else { + specs["KmeshSendmsgCompat"] = coll.Maps + } + } else { + if coll, err := dualengine.LoadKmeshSendmsg(); err != nil { + return nil, fmt.Errorf("load KmeshSendmsg spec: %w", err) + } else { + specs["KmeshSendmsg"] = coll.Maps + } + } + // Symbol KmeshSockopsWorkload has both normal and compat variants (dualengine). + if helper.KernelVersionLowerThan5_13() { + if coll, err := dualengine.LoadKmeshSockopsWorkloadCompat(); err != nil { + return nil, fmt.Errorf("load Compat KmeshSockopsWorkloadCompat spec: %w", err) + } else { + specs["KmeshSockopsWorkloadCompat"] = coll.Maps + } + } else { + if coll, err := dualengine.LoadKmeshSockopsWorkload(); err != nil { + return nil, fmt.Errorf("load KmeshSockopsWorkload spec: %w", err) + } else { + specs["KmeshSockopsWorkload"] = coll.Maps + } + } + // Symbol KmeshXDPAuth has both normal and compat variants (dualengine). + if helper.KernelVersionLowerThan5_13() { + if coll, err := dualengine.LoadKmeshXDPAuthCompat(); err != nil { + return nil, fmt.Errorf("load Compat KmeshXDPAuthCompat spec: %w", err) + } else { + specs["KmeshXDPAuthCompat"] = coll.Maps + } + } else { + if coll, err := dualengine.LoadKmeshXDPAuth(); err != nil { + return nil, fmt.Errorf("load KmeshXDPAuth spec: %w", err) + } else { + specs["KmeshXDPAuth"] = coll.Maps + } + } + } + // General Symbol KmeshTcMarkDecrypt has normal+compat (choose by kernel) + if helper.KernelVersionLowerThan5_13() { + if coll, err := general.LoadKmeshTcMarkDecryptCompat(); err != nil { + return nil, fmt.Errorf("load Compat KmeshTcMarkDecryptCompat spec: %w", err) + } else { + specs["KmeshTcMarkDecryptCompat"] = coll.Maps + } + } else { + if coll, err := general.LoadKmeshTcMarkDecrypt(); err != nil { + return nil, fmt.Errorf("load KmeshTcMarkDecrypt spec: %w", err) + } else { + specs["KmeshTcMarkDecrypt"] = coll.Maps + } + } + // General Symbol KmeshTcMarkEncrypt has normal+compat (choose by kernel) + if helper.KernelVersionLowerThan5_13() { + if coll, err := general.LoadKmeshTcMarkEncryptCompat(); err != nil { + return nil, fmt.Errorf("load Compat KmeshTcMarkEncryptCompat spec: %w", err) + } else { + specs["KmeshTcMarkEncryptCompat"] = coll.Maps + } + } else { + if coll, err := general.LoadKmeshTcMarkEncrypt(); err != nil { + return nil, fmt.Errorf("load KmeshTcMarkEncrypt spec: %w", err) + } else { + specs["KmeshTcMarkEncrypt"] = coll.Maps + } + } + + return specs, nil +} diff --git a/pkg/bpf/restart/new_version_mapspec_loader_enhanced.go b/pkg/bpf/restart/new_version_mapspec_loader_enhanced.go new file mode 100644 index 000000000..c0c1a4fd5 --- /dev/null +++ b/pkg/bpf/restart/new_version_mapspec_loader_enhanced.go @@ -0,0 +1,101 @@ +//go:build enhanced +// +build enhanced + +/* + * Copyright The Kmesh Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Code generated by hack/gen_bpf_specs.go; DO NOT EDIT. + +package restart + +import ( + "fmt" + + "github.com/cilium/ebpf" + + dualengine "kmesh.net/kmesh/bpf/kmesh/bpf2go/dualengine" + general "kmesh.net/kmesh/bpf/kmesh/bpf2go/general" + kernelnative_enhanced "kmesh.net/kmesh/bpf/kmesh/bpf2go/kernelnative/enhanced" + "kmesh.net/kmesh/daemon/options" + helper "kmesh.net/kmesh/pkg/utils" +) + +// Auto-generated: keeps in sync with //go:generate bpf2go lines. +func LoadCompileTimeSpecs(config *options.BpfConfig) (map[string]map[string]*ebpf.MapSpec, error) { + specs := make(map[string]map[string]*ebpf.MapSpec) + + if config.KernelNativeEnabled() { + // Symbol KmeshCgroupSock has both normal and compat variants. + if helper.KernelVersionLowerThan5_13() { + if coll, err := kernelnative_enhanced.LoadKmeshCgroupSockCompat(); err != nil { + return nil, fmt.Errorf("load Compat KmeshCgroupSockCompat spec: %w", err) + } else { + specs["KmeshCgroupSockCompat"] = coll.Maps + } + } else { + if coll, err := kernelnative_enhanced.LoadKmeshCgroupSock(); err != nil { + return nil, fmt.Errorf("load KmeshCgroupSock spec: %w", err) + } else { + specs["KmeshCgroupSock"] = coll.Maps + } + } + // Symbol KmeshSockops has both normal and compat variants. + if helper.KernelVersionLowerThan5_13() { + if coll, err := kernelnative_enhanced.LoadKmeshSockopsCompat(); err != nil { + return nil, fmt.Errorf("load Compat KmeshSockopsCompat spec: %w", err) + } else { + specs["KmeshSockopsCompat"] = coll.Maps + } + } else { + if coll, err := kernelnative_enhanced.LoadKmeshSockops(); err != nil { + return nil, fmt.Errorf("load KmeshSockops spec: %w", err) + } else { + specs["KmeshSockops"] = coll.Maps + } + } + } else if config.DualEngineEnabled() { + } + // General Symbol KmeshTcMarkDecrypt has normal+compat (choose by kernel) + if helper.KernelVersionLowerThan5_13() { + if coll, err := general.LoadKmeshTcMarkDecryptCompat(); err != nil { + return nil, fmt.Errorf("load Compat KmeshTcMarkDecryptCompat spec: %w", err) + } else { + specs["KmeshTcMarkDecryptCompat"] = coll.Maps + } + } else { + if coll, err := general.LoadKmeshTcMarkDecrypt(); err != nil { + return nil, fmt.Errorf("load KmeshTcMarkDecrypt spec: %w", err) + } else { + specs["KmeshTcMarkDecrypt"] = coll.Maps + } + } + // General Symbol KmeshTcMarkEncrypt has normal+compat (choose by kernel) + if helper.KernelVersionLowerThan5_13() { + if coll, err := general.LoadKmeshTcMarkEncryptCompat(); err != nil { + return nil, fmt.Errorf("load Compat KmeshTcMarkEncryptCompat spec: %w", err) + } else { + specs["KmeshTcMarkEncryptCompat"] = coll.Maps + } + } else { + if coll, err := general.LoadKmeshTcMarkEncrypt(); err != nil { + return nil, fmt.Errorf("load KmeshTcMarkEncrypt spec: %w", err) + } else { + specs["KmeshTcMarkEncrypt"] = coll.Maps + } + } + + return specs, nil +} diff --git a/pkg/bpf/workload/skb.go b/pkg/bpf/workload/skb.go index 88e1b1802..3db979817 100644 --- a/pkg/bpf/workload/skb.go +++ b/pkg/bpf/workload/skb.go @@ -122,7 +122,7 @@ func (cs *BpfCroupSkbWorkload) Attach() error { } pinPathIn := filepath.Join(cs.Info.BpfFsPath, "cgroup_skb_ingress_prog") pinPathEg := filepath.Join(cs.Info.BpfFsPath, "cgroup_skb_egress_prog") - if restart.GetStartType() == restart.Restart { + if restart.GetStartType() == restart.Restart || restart.GetStartType() == restart.Update { if cs.Link, err = utils.BpfProgUpdate(pinPathIn, cgopt); err != nil { return err } diff --git a/pkg/bpf/workload/sock_connection.go b/pkg/bpf/workload/sock_connection.go index ea254c73c..fdbe316bb 100644 --- a/pkg/bpf/workload/sock_connection.go +++ b/pkg/bpf/workload/sock_connection.go @@ -169,7 +169,7 @@ func (sc *SockConnWorkload) Attach() error { pinPathDns4 := filepath.Join(sc.Info.BpfFsPath, "sockconn_dns_prog") pinPathDnsRecv4 := filepath.Join(sc.Info.BpfFsPath, "sockconn_dns_recv_prog") - if restart.GetStartType() == restart.Restart { + if restart.GetStartType() == restart.Restart || restart.GetStartType() == restart.Update { if sc.Link, err = utils.BpfProgUpdate(pinPath4, cgopt4); err != nil { return err } diff --git a/pkg/bpf/workload/sock_ops.go b/pkg/bpf/workload/sock_ops.go index b6211b722..02ff479d0 100644 --- a/pkg/bpf/workload/sock_ops.go +++ b/pkg/bpf/workload/sock_ops.go @@ -112,7 +112,7 @@ func (so *BpfSockOpsWorkload) Attach() error { } pinPath := filepath.Join(so.Info.BpfFsPath, "cgroup_sockops_prog") - if restart.GetStartType() == restart.Restart { + if restart.GetStartType() == restart.Restart || restart.GetStartType() == restart.Update { if so.Link, err = utils.BpfProgUpdate(pinPath, cgopt); err != nil { return err } diff --git a/test/e2e/run_test.sh b/test/e2e/run_test.sh index 74fa51bb5..0fabb50ee 100755 --- a/test/e2e/run_test.sh +++ b/test/e2e/run_test.sh @@ -176,6 +176,55 @@ function setup_kmesh() { done } +function set_daemonupgarde_testcase_image() { + local TMP_BUILD + TMP_BUILD="$(mktemp -d)" + echo "Building in temp dir: $TMP_BUILD" + + git clone --depth 1 . "$TMP_BUILD" || { + echo "git clone failed" + rm -rf "$TMP_BUILD" + return 1 + } + + pushd "$TMP_BUILD" >/dev/null + + BPF_HEADER_FILE="./bpf/include/bpf_common.h" + echo "Modifying BPF header file: ${BPF_HEADER_FILE}" + + sed -i \ + '/} kmesh_map64 SEC(".maps");/a\ + \ +struct {\ + __uint(type, BPF_MAP_TYPE_HASH);\ + __uint(key_size, sizeof(__u32));\ + __uint(value_size, MAP_VAL_SIZE_64);\ + __uint(max_entries, MAP_MAX_ENTRIES);\ + __uint(map_flags, BPF_F_NO_PREALLOC);\ +} kmesh_map64_bak_for_test SEC(".maps");' \ + "${BPF_HEADER_FILE}" + + local HUB="localhost:5000" + local TARGET="kmesh" + local TAG="test-upgrade-map-change" + local IMAGE="${HUB}/${TARGET}:${TAG}" + + echo "Running 'make docker.push' with custom HUB and TAG in $TMP_BUILD" + if ! HUB=${HUB} TARGET=${TARGET} TAG=${TAG} make docker.push; then + echo "make docker.push failed" + popd >/dev/null + rm -rf "$TMP_BUILD" + return 1 + fi + + export KMESH_UPGRADE_IMAGE="${IMAGE}" + echo "Built and pushed image: ${IMAGE}" + + popd >/dev/null + # rm -rf "$TMP_BUILD" + return 0 +} + function setup_kmesh_log() { # Set log of each Kmesh pods. PODS=$(kubectl get pods -n kmesh-system -l app=kmesh -o jsonpath='{.items[*].metadata.name}') @@ -346,6 +395,10 @@ while (("$#")); do PARAMS+=("-istio.test.nocleanup") shift ;; + --skip-build-daemonupgarde-image) + SKIP_BUILD_UPGARDE=true + shift + ;; *) PARAMS+=("$1") shift @@ -367,6 +420,9 @@ if [[ -z ${SKIP_BUILD:-} ]]; then setup_kind_registry build_and_push_images install_kmeshctl + if [[ -z ${SKIP_BUILD_UPGARDE:-} ]]; then + set_daemonupgarde_testcase_image + fi fi kubectl config use-context "kind-$NAME" @@ -403,6 +459,6 @@ if [[ -n ${CLEANUP_REGISTRY} ]]; then cleanup_docker_registry fi -rm -rf "${TMP}" +#rm -rf "${TMP}" exit $EXIT_CODE diff --git a/test/e2e/upgarde_test.go b/test/e2e/upgarde_test.go new file mode 100644 index 000000000..58d59b03c --- /dev/null +++ b/test/e2e/upgarde_test.go @@ -0,0 +1,137 @@ +//go:build integ +// +build integ + +/* + * Copyright The Kmesh Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// NOTE: THE CODE IN THIS FILE IS MAINLY REFERENCED FROM ISTIO INTEGRATION +// FRAMEWORK(https://github.com/istio/istio/tree/master/tests/integration) +// AND ADAPTED FOR KMESH. + +package kmesh + +import ( + "context" + "fmt" + "os" + "testing" + "time" + + "istio.io/istio/pkg/test/framework" + "istio.io/istio/pkg/test/framework/components/echo" + "istio.io/istio/pkg/test/framework/components/echo/util/traffic" + kubetest "istio.io/istio/pkg/test/kube" + "istio.io/istio/pkg/test/util/retry" + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +// TestKmeshUpgrade performs a rolling upgrade of the kmesh daemonset image +// while continuous traffic is flowing, and asserts there is no traffic disruption. +func TestKmeshUpgrade(t *testing.T) { + framework.NewTest(t).Run(func(t framework.TestContext) { + configureDNSProxy(t, false) + + src := apps.EnrolledToKmesh[0] + dst := apps.ServiceWithWaypointAtServiceGranularity + options := echo.CallOptions{ + To: dst, + Count: 1, + Check: httpValidator, + Port: echo.Port{ + Name: "http", + }, + Retry: echo.Retry{NoRetry: true}, + } + + g := traffic.NewGenerator(t, traffic.Config{ + Source: src, + Options: options, + Interval: 50 * time.Millisecond, + }).Start() + + upgradeKmesh(t) + + g.Stop().CheckSuccessRate(t, 1) + + configureDNSProxy(t, true) + }) +} + +// upgradeKmesh patches the daemonset image to the value of KMESH_UPGRADE_IMAGE and waits for rollout. +func upgradeKmesh(t framework.TestContext) { + newImage := os.Getenv("KMESH_UPGRADE_IMAGE") + if newImage == "" { + newImage = "localhost:5000/kmesh" + } + + patchData := fmt.Sprintf(`{ + "spec": { + "template": { + "metadata": { + "annotations": { + "kmesh-upgrade-at": %q + } + }, + "spec": { + "containers": [ + { + "name": "kmesh", + "image": "%s" + } + ] + } + } + } + }`, time.Now().Format(time.RFC3339), newImage) + + patchKmesh_upgrade(t, patchData) +} + +// patchKmesh applies a strategic merge patch to the Kmesh DaemonSet and waits for rollout completion. +func patchKmesh_upgrade(t framework.TestContext, patchData string) { + patchOpts := metav1.PatchOptions{} + ds := t.Clusters().Default().Kube().AppsV1().DaemonSets(KmeshNamespace) + _, err := ds.Patch(context.Background(), KmeshDaemonsetName, types.StrategicMergePatchType, []byte(patchData), patchOpts) + if err != nil { + t.Fatal(err) + } + + if err := retry.UntilSuccess(func() error { + d, err := ds.Get(context.Background(), KmeshDaemonsetName, metav1.GetOptions{}) + if err != nil { + return err + } + if !daemonsetsetComplete_upgrade(d) { + return fmt.Errorf("rollout is not yet done") + } + return nil + }, retry.Timeout(120*time.Second), retry.Delay(2*time.Second)); err != nil { + t.Fatalf("failed to wait for Kmesh rollout status: %v", err) + } + + if _, err := kubetest.CheckPodsAreReady(kubetest.NewPodFetch(t.AllClusters()[0], KmeshNamespace, "app=kmesh")); err != nil { + t.Fatal(err) + } +} + +// daemonsetsetComplete returns true when DaemonSet rollout appears complete. +func daemonsetsetComplete_upgrade(ds *appsv1.DaemonSet) bool { + return ds.Status.UpdatedNumberScheduled == ds.Status.DesiredNumberScheduled && + ds.Status.NumberReady == ds.Status.DesiredNumberScheduled && + ds.Status.ObservedGeneration >= ds.Generation +}