From d4d91c45267ade4e09971d10eab23c5c582f57be Mon Sep 17 00:00:00 2001 From: zhengyilei Date: Fri, 26 Jun 2026 14:15:53 +0800 Subject: [PATCH] fix(CubeMaster): preserve template network rules and resource defaults Preserve egress rules when cloning template create requests so accepted network policy is not lost during template creation from image. Also preserve the first resource validation error when both CPU and memory exceed configured limits. Update egress rule merge order to honor request-side overrides under first-match-wins semantics, and keep the English and Chinese network policy docs aligned with the implemented behavior. Signed-off-by: zhengyilei --- .../service/httpservice/cube/cubeboxutil.go | 51 +++-- .../httpservice/cube/cubeboxutil_test.go | 203 ++++++++++++++++++ CubeMaster/pkg/service/sandbox/util.go | 4 +- CubeMaster/pkg/service/sandbox/util_test.go | 88 ++++++++ .../pkg/templatecenter/template_image_test.go | 85 ++++++++ .../pkg/templatecenter/template_request.go | 56 +++++ docs/guide/network-policy.md | 2 +- docs/zh/guide/network-policy.md | 2 +- 8 files changed, 468 insertions(+), 23 deletions(-) create mode 100644 CubeMaster/pkg/service/httpservice/cube/cubeboxutil_test.go diff --git a/CubeMaster/pkg/service/httpservice/cube/cubeboxutil.go b/CubeMaster/pkg/service/httpservice/cube/cubeboxutil.go index 7d3dad32f..813fc7b91 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 000000000..4dc6027c4 --- /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 01e2f8a00..e847bcf65 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 a5d549358..90a096f0a 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 f6632c1fb..62341a7a2 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 9da5556e1..116db4a41 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 6f910e62f..9c53f9581 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 8c05bba99..98b1510f3 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。