diff --git a/CubeMaster/pkg/service/httpservice/cube/cubeboxutil.go b/CubeMaster/pkg/service/httpservice/cube/cubeboxutil.go index 7d3dad32..813fc7b9 100644 --- a/CubeMaster/pkg/service/httpservice/cube/cubeboxutil.go +++ b/CubeMaster/pkg/service/httpservice/cube/cubeboxutil.go @@ -264,7 +264,7 @@ func mergeCubeNetworkConfigs(templateCfg *types.CubeNetworkConfig, requestCfg *t return cloneCubeNetworkConfig(templateCfg) } - out := cloneCubeNetworkConfig(templateCfg) + out := cloneCubeNetworkConfigBase(templateCfg) if requestCfg.AllowInternetAccess != nil { allowInternetAccess := *requestCfg.AllowInternetAccess out.AllowInternetAccess = &allowInternetAccess @@ -285,19 +285,29 @@ func mergeCubeNetworkConfigs(templateCfg *types.CubeNetworkConfig, requestCfg *t out.DenyOut = appendUniqueCIDRs(out.DenyOut, requestCfg.DenyOut) } if len(requestCfg.Rules) > 0 { - out.Rules = mergeEgressRules(out.Rules, requestCfg.Rules) + out.Rules = mergeEgressRules(templateCfg.Rules, requestCfg.Rules) + } else { + out.Rules = cloneEgressRules(templateCfg.Rules) } return out } func cloneCubeNetworkConfig(in *types.CubeNetworkConfig) *types.CubeNetworkConfig { + out := cloneCubeNetworkConfigBase(in) + if out == nil { + return nil + } + out.Rules = cloneEgressRules(in.Rules) + return out +} + +func cloneCubeNetworkConfigBase(in *types.CubeNetworkConfig) *types.CubeNetworkConfig { if in == nil { return nil } out := &types.CubeNetworkConfig{ AllowOut: append([]string(nil), in.AllowOut...), DenyOut: append([]string(nil), in.DenyOut...), - Rules: cloneEgressRules(in.Rules), } if in.AllowInternetAccess != nil { allowInternetAccess := *in.AllowInternetAccess @@ -310,32 +320,35 @@ func cloneCubeNetworkConfig(in *types.CubeNetworkConfig) *types.CubeNetworkConfi return out } -// mergeEgressRules combines template + request rules. Rules sharing the same -// Name are overridden by the request side; otherwise request rules are -// appended after template rules to preserve first-match-wins ordering with -// the template's policy taking precedence on overlap. +// mergeEgressRules combines template + request rules. Because egress rules are +// first-match-wins, per-sandbox/request rules must come before template rules. +// Template rules whose Name is overridden by the request side are skipped. func mergeEgressRules(base []*types.EgressRule, extra []*types.EgressRule) []*types.EgressRule { if len(extra) == 0 { - return base + return cloneEgressRules(base) } - indexByName := make(map[string]int, len(base)) - out := cloneEgressRules(base) - for i, r := range out { - if r != nil { - indexByName[r.Name] = i - } + if len(base) == 0 { + return cloneEgressRules(extra) } + + out := make([]*types.EgressRule, 0, len(extra)+len(base)) + overridden := make(map[string]struct{}, len(extra)) for _, r := range extra { if r == nil { continue } - cloned := cloneEgressRule(r) - if idx, ok := indexByName[r.Name]; ok { - out[idx] = cloned + out = append(out, cloneEgressRule(r)) + overridden[r.Name] = struct{}{} + } + + for _, r := range base { + if r == nil { continue } - indexByName[r.Name] = len(out) - out = append(out, cloned) + if _, ok := overridden[r.Name]; ok { + continue + } + out = append(out, cloneEgressRule(r)) } return out } diff --git a/CubeMaster/pkg/service/httpservice/cube/cubeboxutil_test.go b/CubeMaster/pkg/service/httpservice/cube/cubeboxutil_test.go new file mode 100644 index 00000000..4dc6027c --- /dev/null +++ b/CubeMaster/pkg/service/httpservice/cube/cubeboxutil_test.go @@ -0,0 +1,203 @@ +// Copyright (c) 2026 Tencent Inc. +// SPDX-License-Identifier: Apache-2.0 +// + +package cube + +import ( + "testing" + + "github.com/tencentcloud/CubeSandbox/CubeMaster/pkg/service/sandbox/types" +) + +func TestMergeEgressRulesPrependsRequestRulesAndOverridesTemplateByName(t *testing.T) { + templateRules := []*types.EgressRule{ + { + Name: "shared", + Match: &types.EgressRuleMatch{ + Host: strPtr("template.example.com"), + }, + Action: &types.EgressRuleAction{Allow: false}, + }, + { + Name: "template-only", + Match: &types.EgressRuleMatch{ + Host: strPtr("template-only.example.com"), + }, + Action: &types.EgressRuleAction{Allow: true}, + }, + } + requestRules := []*types.EgressRule{ + { + Name: "shared", + Match: &types.EgressRuleMatch{ + Host: strPtr("request.example.com"), + }, + Action: &types.EgressRuleAction{Allow: true}, + }, + { + Name: "request-only", + Match: &types.EgressRuleMatch{ + Host: strPtr("request-only.example.com"), + }, + Action: &types.EgressRuleAction{Allow: false}, + }, + } + + got := mergeEgressRules(templateRules, requestRules) + if len(got) != 3 { + t.Fatalf("expected 3 merged rules, got %d", len(got)) + } + if got[0].Name != "shared" || got[1].Name != "request-only" || got[2].Name != "template-only" { + t.Fatalf("unexpected merged rule order: %#v", []string{got[0].Name, got[1].Name, got[2].Name}) + } + if got[0] == requestRules[0] || got[1] == requestRules[1] || got[2] == templateRules[1] { + t.Fatal("expected merged rules to be cloned, got shared pointers") + } + if got[0].Match == nil || got[0].Match.Host == nil || *got[0].Match.Host != "request.example.com" { + t.Fatalf("expected request override to win for shared rule, got %+v", got[0]) + } +} + +func TestMergeEgressRulesPrependsRequestRulesWithoutNameConflict(t *testing.T) { + templateRules := []*types.EgressRule{ + {Name: "template-a", Action: &types.EgressRuleAction{Allow: true}}, + {Name: "template-b", Action: &types.EgressRuleAction{Allow: false}}, + } + requestRules := []*types.EgressRule{ + {Name: "request-a", Action: &types.EgressRuleAction{Allow: true}}, + {Name: "request-b", Action: &types.EgressRuleAction{Allow: false}}, + } + + got := mergeEgressRules(templateRules, requestRules) + if len(got) != 4 { + t.Fatalf("expected 4 merged rules, got %d", len(got)) + } + gotNames := []string{got[0].Name, got[1].Name, got[2].Name, got[3].Name} + wantNames := []string{"request-a", "request-b", "template-a", "template-b"} + for i := range wantNames { + if gotNames[i] != wantNames[i] { + t.Fatalf("unexpected merged rule order: got=%v want=%v", gotNames, wantNames) + } + } + if got[0] == requestRules[0] || got[1] == requestRules[1] || got[2] == templateRules[0] || got[3] == templateRules[1] { + t.Fatal("expected merged rules to be cloned, got shared pointers") + } +} + +func TestMergeEgressRulesHandlesEmptySides(t *testing.T) { + templateRules := []*types.EgressRule{ + {Name: "template-a", Match: &types.EgressRuleMatch{Host: strPtr("template.example.com")}}, + } + requestRules := []*types.EgressRule{ + {Name: "request-a", Match: &types.EgressRuleMatch{Host: strPtr("request.example.com")}}, + } + + gotTemplateOnly := mergeEgressRules(templateRules, nil) + if len(gotTemplateOnly) != 1 || gotTemplateOnly[0].Name != "template-a" { + t.Fatalf("unexpected template-only merge result: %+v", gotTemplateOnly) + } + if gotTemplateOnly[0] == templateRules[0] { + t.Fatal("expected template-only merge to clone rules") + } + + gotRequestOnly := mergeEgressRules(nil, requestRules) + if len(gotRequestOnly) != 1 || gotRequestOnly[0].Name != "request-a" { + t.Fatalf("unexpected request-only merge result: %+v", gotRequestOnly) + } + if gotRequestOnly[0] == requestRules[0] { + t.Fatal("expected request-only merge to clone rules") + } +} + +func TestMergeEgressRulesSkipsNilEntries(t *testing.T) { + templateRules := []*types.EgressRule{ + nil, + {Name: "template-a", Action: &types.EgressRuleAction{Allow: true}}, + } + requestRules := []*types.EgressRule{ + nil, + {Name: "request-a", Action: &types.EgressRuleAction{Allow: false}}, + } + + got := mergeEgressRules(templateRules, requestRules) + if len(got) != 2 { + t.Fatalf("expected nil entries to be skipped, got %d merged rules", len(got)) + } + if got[0].Name != "request-a" || got[1].Name != "template-a" { + t.Fatalf("unexpected merged rule order with nil entries: %#v", []string{got[0].Name, got[1].Name}) + } +} + +func TestMergeCubeNetworkConfigsMergesRulesWithoutAliasingTemplate(t *testing.T) { + templateAllow := false + requestAllow := true + templateCfg := &types.CubeNetworkConfig{ + AllowInternetAccess: &templateAllow, + AllowOut: []string{"10.0.0.0/8"}, + DenyOut: []string{"192.168.0.0/16"}, + Rules: []*types.EgressRule{ + {Name: "template-only", Match: &types.EgressRuleMatch{Host: strPtr("template.example.com")}}, + }, + } + requestCfg := &types.CubeNetworkConfig{ + AllowInternetAccess: &requestAllow, + AllowOut: []string{"172.16.0.0/12"}, + DenyOut: []string{"100.64.0.0/10"}, + Rules: []*types.EgressRule{ + {Name: "request-only", Match: &types.EgressRuleMatch{Host: strPtr("request.example.com")}}, + }, + } + + got := mergeCubeNetworkConfigs(templateCfg, requestCfg) + if got == nil { + t.Fatal("expected merged config, got nil") + } + if got.AllowInternetAccess == nil || !*got.AllowInternetAccess { + t.Fatalf("expected request allowInternetAccess override, got %+v", got.AllowInternetAccess) + } + if len(got.AllowOut) != 2 || got.AllowOut[0] != "10.0.0.0/8" || got.AllowOut[1] != "172.16.0.0/12" { + t.Fatalf("unexpected merged allowOut: %#v", got.AllowOut) + } + if len(got.DenyOut) != 2 || got.DenyOut[0] != "192.168.0.0/16" || got.DenyOut[1] != "100.64.0.0/10" { + t.Fatalf("unexpected merged denyOut: %#v", got.DenyOut) + } + if len(got.Rules) != 2 || got.Rules[0].Name != "request-only" || got.Rules[1].Name != "template-only" { + t.Fatalf("unexpected merged rules: %#v", []string{got.Rules[0].Name, got.Rules[1].Name}) + } + if got.Rules[0] == requestCfg.Rules[0] || got.Rules[1] == templateCfg.Rules[0] { + t.Fatal("expected merged rules to be cloned, got shared pointers") + } +} + +func TestMergeCubeNetworkConfigsClonesTemplateRulesWhenRequestRulesEmpty(t *testing.T) { + templateAllow := false + requestAllow := true + templateCfg := &types.CubeNetworkConfig{ + AllowInternetAccess: &templateAllow, + Rules: []*types.EgressRule{ + {Name: "template-only", Match: &types.EgressRuleMatch{Host: strPtr("template.example.com")}}, + }, + } + requestCfg := &types.CubeNetworkConfig{ + AllowInternetAccess: &requestAllow, + } + + got := mergeCubeNetworkConfigs(templateCfg, requestCfg) + if got == nil { + t.Fatal("expected merged config, got nil") + } + if got.AllowInternetAccess == nil || !*got.AllowInternetAccess { + t.Fatalf("expected request allowInternetAccess override, got %+v", got.AllowInternetAccess) + } + if len(got.Rules) != 1 || got.Rules[0].Name != "template-only" { + t.Fatalf("unexpected merged rules: %#v", got.Rules) + } + if got.Rules[0] == templateCfg.Rules[0] { + t.Fatal("expected template rules to be cloned when request has no rules") + } +} + +func strPtr(s string) *string { + return &s +} diff --git a/CubeMaster/pkg/service/sandbox/util.go b/CubeMaster/pkg/service/sandbox/util.go index 01e2f8a0..e847bcf6 100644 --- a/CubeMaster/pkg/service/sandbox/util.go +++ b/CubeMaster/pkg/service/sandbox/util.go @@ -107,11 +107,11 @@ func getReqResource(req *types.CreateCubeSandboxReq) (cpu, mem resource.Quantity if config.GetConfig().Scheduler != nil { if cpu.Cmp(config.GetConfig().Scheduler.MaxMvmCPURes()) >= 0 { - err = ret.Errorf(errorcode.ErrorCode_MasterParamsError, "request Resources cpu[%dm] is invalid", + return cpu, mem, ret.Errorf(errorcode.ErrorCode_MasterParamsError, "request Resources cpu[%dm] is invalid", cpu.MilliValue()) } if mem.Cmp(config.GetConfig().Scheduler.MaxMvmMemoryRes()) >= 0 { - err = ret.Errorf(errorcode.ErrorCode_MasterParamsError, "request Resources mem[%dKB] is invalid", + return cpu, mem, ret.Errorf(errorcode.ErrorCode_MasterParamsError, "request Resources mem[%dKB] is invalid", mem.Value()/1024) } } diff --git a/CubeMaster/pkg/service/sandbox/util_test.go b/CubeMaster/pkg/service/sandbox/util_test.go index a5d54935..90a096f0 100644 --- a/CubeMaster/pkg/service/sandbox/util_test.go +++ b/CubeMaster/pkg/service/sandbox/util_test.go @@ -5,13 +5,37 @@ package sandbox import ( + "os" + "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" cubebox "github.com/tencentcloud/CubeSandbox/CubeMaster/api/services/cubebox/v1" + "github.com/tencentcloud/CubeSandbox/CubeMaster/pkg/base/config" "github.com/tencentcloud/CubeSandbox/CubeMaster/pkg/service/sandbox/types" ) +func ensureSandboxTestConfig(t *testing.T) *config.Config { + t.Helper() + if cfg := config.GetConfig(); cfg != nil { + return cfg + } + mydir, err := os.Getwd() + if err != nil { + t.Fatalf("getwd failed: %v", err) + } + cfgPath := filepath.Clean(filepath.Join(mydir, "../../../conf.yaml")) + if err := os.Setenv("CUBE_MASTER_CONFIG_PATH", cfgPath); err != nil { + t.Fatalf("set CUBE_MASTER_CONFIG_PATH failed: %v", err) + } + cfg, err := config.Init() + if err != nil { + t.Fatalf("config.Init failed: %v", err) + } + return cfg +} + func Test_checkAndGetHostDirVolumeSource(t *testing.T) { type args struct { src *types.HostDirVolumeSources @@ -115,3 +139,67 @@ func Test_checkAndGetHostDirVolumeSource(t *testing.T) { }) } } + +func TestGetReqResourceRejectsCPUOverflowWhenMemIsValid(t *testing.T) { + cfg := ensureSandboxTestConfig(t) + origScheduler := cfg.Scheduler + cfg.Scheduler = &config.WrapperSchedulerConf{ + SchedulerConf: config.SchedulerConf{ + MaxMvmCPU: "1", + MaxMvmMemory: "8Gi", + }, + } + defer func() { + cfg.Scheduler = origScheduler + }() + + req := &types.CreateCubeSandboxReq{ + Containers: []*types.Container{{ + Name: "ctr-1", + Resources: &types.Resource{ + Cpu: "2", + Mem: "1Gi", + }, + }}, + } + + _, _, err := getReqResource(req) + if err == nil { + t.Fatal("expected cpu overflow to return error") + } + if !strings.Contains(err.Error(), "cpu") { + t.Fatalf("expected cpu validation error, got %v", err) + } +} + +func TestGetReqResourceRejectsCPUOverflowBeforeMemOverflow(t *testing.T) { + cfg := ensureSandboxTestConfig(t) + origScheduler := cfg.Scheduler + cfg.Scheduler = &config.WrapperSchedulerConf{ + SchedulerConf: config.SchedulerConf{ + MaxMvmCPU: "1", + MaxMvmMemory: "8Gi", + }, + } + defer func() { + cfg.Scheduler = origScheduler + }() + + req := &types.CreateCubeSandboxReq{ + Containers: []*types.Container{{ + Name: "ctr-1", + Resources: &types.Resource{ + Cpu: "2", + Mem: "9Gi", + }, + }}, + } + + _, _, err := getReqResource(req) + if err == nil { + t.Fatal("expected cpu and mem overflow to return error") + } + if !strings.Contains(err.Error(), "cpu") { + t.Fatalf("expected cpu validation error to win, got %v", err) + } +} diff --git a/CubeMaster/pkg/templatecenter/template_image_test.go b/CubeMaster/pkg/templatecenter/template_image_test.go index f6632c1f..62341a7a 100644 --- a/CubeMaster/pkg/templatecenter/template_image_test.go +++ b/CubeMaster/pkg/templatecenter/template_image_test.go @@ -578,6 +578,91 @@ func TestGenerateTemplateCreateRequestAppliesDNSConfigOverride(t *testing.T) { } } +func TestGenerateTemplateCreateRequestClonesCubeNetworkRules(t *testing.T) { + allowInternetAccess := false + sni := "sni.example.com" + host := "api.example.com" + path := "/v1" + scheme := "https" + audit := "log-only" + format := "bearer %s" + req := &types.CreateTemplateFromImageReq{ + Request: &types.Request{RequestID: "req-1"}, + SourceImageRef: "docker.io/library/nginx:latest", + TemplateID: "template-1", + WritableLayerSize: "20Gi", + InstanceType: cubeboxv1.InstanceType_cubebox.String(), + NetworkType: cubeboxv1.NetworkType_tap.String(), + CubeNetworkConfig: &types.CubeNetworkConfig{ + AllowInternetAccess: &allowInternetAccess, + Rules: []*types.EgressRule{{ + Name: "allow-api", + Match: &types.EgressRuleMatch{ + SNI: &sni, + Host: &host, + Method: []string{"GET"}, + Path: &path, + Scheme: &scheme, + }, + Action: &types.EgressRuleAction{ + Allow: true, + Audit: &audit, + Inject: []*types.EgressRuleInject{{ + Header: "Authorization", + Secret: "secret-id", + Format: &format, + }}, + }, + }}, + }, + } + artifact := &models.RootfsArtifact{ + ArtifactID: "artifact-1", + TemplateSpecFingerprint: "fingerprint-1", + Ext4SHA256: "sha256-1", + Ext4SizeBytes: 1024, + DownloadToken: "token-1", + } + + got, err := generateTemplateCreateRequest(req, artifact, image.DockerImageConfig{}, "http://master.example") + if err != nil { + t.Fatalf("generateTemplateCreateRequest failed: %v", err) + } + if got.CubeNetworkConfig == nil { + t.Fatal("expected CubeNetworkConfig to be propagated") + } + if len(got.CubeNetworkConfig.Rules) != 1 { + t.Fatalf("expected 1 egress rule, got %d", len(got.CubeNetworkConfig.Rules)) + } + if got.CubeNetworkConfig.Rules[0] == req.CubeNetworkConfig.Rules[0] { + t.Fatal("expected egress rule to be cloned, got shared pointer") + } + if got.CubeNetworkConfig.Rules[0].Match == nil || got.CubeNetworkConfig.Rules[0].Match.Host == nil || *got.CubeNetworkConfig.Rules[0].Match.Host != "api.example.com" { + t.Fatalf("unexpected cloned egress rule: %+v", got.CubeNetworkConfig.Rules[0]) + } + if got.CubeNetworkConfig.Rules[0].Match == req.CubeNetworkConfig.Rules[0].Match { + t.Fatal("expected egress rule match to be cloned, got shared pointer") + } + if got.CubeNetworkConfig.Rules[0].Match.SNI == req.CubeNetworkConfig.Rules[0].Match.SNI || + got.CubeNetworkConfig.Rules[0].Match.Host == req.CubeNetworkConfig.Rules[0].Match.Host || + got.CubeNetworkConfig.Rules[0].Match.Path == req.CubeNetworkConfig.Rules[0].Match.Path || + got.CubeNetworkConfig.Rules[0].Match.Scheme == req.CubeNetworkConfig.Rules[0].Match.Scheme { + t.Fatal("expected egress rule match string pointers to be deep-cloned") + } + if got.CubeNetworkConfig.Rules[0].Action == nil || got.CubeNetworkConfig.Rules[0].Action == req.CubeNetworkConfig.Rules[0].Action { + t.Fatal("expected egress rule action to be cloned") + } + if got.CubeNetworkConfig.Rules[0].Action.Audit == req.CubeNetworkConfig.Rules[0].Action.Audit { + t.Fatal("expected egress rule audit pointer to be deep-cloned") + } + if len(got.CubeNetworkConfig.Rules[0].Action.Inject) != 1 || got.CubeNetworkConfig.Rules[0].Action.Inject[0] == req.CubeNetworkConfig.Rules[0].Action.Inject[0] { + t.Fatal("expected egress rule inject entries to be cloned") + } + if got.CubeNetworkConfig.Rules[0].Action.Inject[0].Format == req.CubeNetworkConfig.Rules[0].Action.Inject[0].Format { + t.Fatal("expected egress rule inject format pointer to be deep-cloned") + } +} + func TestMarshalTemplateImageJobRequestIgnoresRequestIDAndPassword(t *testing.T) { reqA := &types.CreateTemplateFromImageReq{ Request: &types.Request{RequestID: "req-a"}, diff --git a/CubeMaster/pkg/templatecenter/template_request.go b/CubeMaster/pkg/templatecenter/template_request.go index 9da5556e..116db4a4 100644 --- a/CubeMaster/pkg/templatecenter/template_request.go +++ b/CubeMaster/pkg/templatecenter/template_request.go @@ -134,6 +134,7 @@ func cloneCubeNetworkConfig(in *types.CubeNetworkConfig) *types.CubeNetworkConfi out := &types.CubeNetworkConfig{ AllowOut: append([]string(nil), in.AllowOut...), DenyOut: append([]string(nil), in.DenyOut...), + Rules: cloneEgressRules(in.Rules), } if in.AllowInternetAccess != nil { allowInternetAccess := *in.AllowInternetAccess @@ -142,6 +143,61 @@ func cloneCubeNetworkConfig(in *types.CubeNetworkConfig) *types.CubeNetworkConfi return out } +func cloneEgressRules(in []*types.EgressRule) []*types.EgressRule { + if len(in) == 0 { + return nil + } + out := make([]*types.EgressRule, 0, len(in)) + for _, r := range in { + out = append(out, cloneEgressRule(r)) + } + return out +} + +func cloneEgressRule(in *types.EgressRule) *types.EgressRule { + if in == nil { + return nil + } + out := &types.EgressRule{Name: in.Name} + if in.Match != nil { + out.Match = &types.EgressRuleMatch{ + SNI: cloneStringPtr(in.Match.SNI), + Host: cloneStringPtr(in.Match.Host), + Method: append([]string(nil), in.Match.Method...), + Path: cloneStringPtr(in.Match.Path), + Scheme: cloneStringPtr(in.Match.Scheme), + } + } + if in.Action != nil { + action := &types.EgressRuleAction{Allow: in.Action.Allow} + action.Audit = cloneStringPtr(in.Action.Audit) + if len(in.Action.Inject) > 0 { + action.Inject = make([]*types.EgressRuleInject, 0, len(in.Action.Inject)) + for _, inj := range in.Action.Inject { + if inj == nil { + continue + } + cloned := &types.EgressRuleInject{ + Header: inj.Header, + Secret: inj.Secret, + Format: cloneStringPtr(inj.Format), + } + action.Inject = append(action.Inject, cloned) + } + } + out.Action = action + } + return out +} + +func cloneStringPtr(in *string) *string { + if in == nil { + return nil + } + out := *in + return &out +} + func formatTemplateImageCubeNetworkConfig(in *types.CubeNetworkConfig) string { if in == nil { return "allow_internet_access=default(true) allow_out=[] deny_out=[] rules=0" diff --git a/docs/guide/network-policy.md b/docs/guide/network-policy.md index 6f910e62..9c53f958 100644 --- a/docs/guide/network-policy.md +++ b/docs/guide/network-policy.md @@ -194,7 +194,7 @@ If the template also carries a `CubeNetworkConfig`, CubeMaster merges template c - `allowInternetAccess`: request value overrides the template value when explicitly set. - `allowOut`: request entries are appended to template entries and deduplicated by string. - `denyOut`: request entries are appended to template entries and deduplicated by string. -- `rules`: merged by `name`. A request rule with the same name overrides the template rule; rules with new names are appended after template rules, preserving first-match-wins order. +- `rules`: merged by `name`. Request rules are placed before template rules. Template rules with the same `name` are dropped, so request rules take precedence under first-match-wins. The merged `CubeNetworkConfig` is sent to Cubelet/network-agent. CubeVS validates the final unique eBPF map keys after network-agent extracts L7 reachability targets. diff --git a/docs/zh/guide/network-policy.md b/docs/zh/guide/network-policy.md index 8c05bba9..98b1510f 100644 --- a/docs/zh/guide/network-policy.md +++ b/docs/zh/guide/network-policy.md @@ -194,7 +194,7 @@ CubeAPI 会把请求映射成 CubeMaster 的 `CubeNetworkConfig`,并转发 `al - `allowInternetAccess`:如果请求中显式设置,则覆盖模板值。 - `allowOut`:把请求列表追加到模板列表后,并按字符串去重。 - `denyOut`:把请求列表追加到模板列表后,并按字符串去重。 -- `rules`:按 `name` 合并。同名规则由请求侧覆盖;不同名规则追加到模板规则后面,保留 first-match-wins 的顺序。 +- `rules`:按 `name` 合并。请求规则排在模板规则前面;同名模板规则会被移除,因此在 first-match-wins 下由请求规则生效。 合并后的 `CubeNetworkConfig` 会发给 Cubelet/network-agent。network-agent 抽取 L7 网络可达目标后,由 CubeVS 校验最终唯一 eBPF map key。