Skip to content

Commit 9d482fa

Browse files
hdurand0710oktalz
authored andcommitted
MINOR: filter startup haproxy objects by LinkID
isUnifiedGatewayManaged now checks the LinkID field in metadata rather than just the presence of the HUG key, so only objects owned by this instance are loaded at startup
1 parent cdedb9d commit 9d482fa

4 files changed

Lines changed: 209 additions & 10 deletions

File tree

cmd/start/start.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ func SetupGateConfig(hugConfig hugconfig.HUGConfig) gateconfig.GateConfigOptions
4242
haproxyConfCh := make(chan diffs.HaproxyConfDiffs, 100)
4343

4444
// Read the haproy.cfg file at startup, and initializes the library with the initial haproxy configuration
45-
initialStructured, err := startup.StructuredFromFile(hugConfig)
45+
linkID := "hug"
46+
initialStructured, err := startup.StructuredFromFile(hugConfig, linkID)
4647
if err != nil {
4748
panic(err)
4849
}
@@ -64,7 +65,7 @@ func SetupGateConfig(hugConfig hugconfig.HUGConfig) gateconfig.GateConfigOptions
6465
opt.IPV4BindAddr(hugConfig.IPV4BindAddr),
6566
opt.IPV6BindAddr(hugConfig.IPV6BindAddr),
6667
opt.HaproxyDirs(hugConfig.HaproxyDirs),
67-
opt.LinkID("hug"),
68+
opt.LinkID(linkID),
6869
opt.InitialStructured(initialStructured),
6970
opt.CacheReSyncPeriod(hugConfig.CacheResyncPeriod),
7071
opt.DefaultsSectionName(constants.DefaultsSectionName),

hug/startup/startup.go

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ type ownerMetaData interface {
3636
// Frontends/Backends
3737
// that have the unified gateway metadata
3838
// (the objects that the gateway manages)
39-
func StructuredFromFile(hugConfig hugconfig.HUGConfig) (structured.Structured, error) {
39+
func StructuredFromFile(hugConfig hugconfig.HUGConfig, linkID string) (structured.Structured, error) {
4040
confClient, err := configuration.New(context.Background(),
4141
cfgoptions.ConfigurationFile(hugConfig.HaproxyDirs.MainCfgFile),
4242
cfgoptions.TransactionsDir(hugConfig.HaproxyDirs.CfgDir),
@@ -58,12 +58,12 @@ func StructuredFromFile(hugConfig hugconfig.HUGConfig) (structured.Structured, e
5858

5959
structuredCfg := structured.NewStructuredConf()
6060
for _, backend := range backends {
61-
if isUnifiedGatewayManaged(backend) {
61+
if isUnifiedGatewayManaged(backend, linkID) {
6262
structuredCfg.Backends[backend.Name] = backend
6363
}
6464
}
6565
for _, frontend := range frontends {
66-
if isUnifiedGatewayManaged(frontend) {
66+
if isUnifiedGatewayManaged(frontend, linkID) {
6767
structuredCfg.Frontends[frontend.Name] = frontend
6868
}
6969
}
@@ -224,9 +224,8 @@ func addBindPortToStatsFrontend(confClient configuration.Configuration,
224224
}
225225

226226
// isUnifiedGatewayManaged returns true if the object is managed by the Unified Gateway
227-
// false otherwise
228-
// based on the MetaData
229-
func isUnifiedGatewayManaged[T ownerMetaData](obj T) bool {
227+
// with the given linkID, false otherwise, based on the MetaData.
228+
func isUnifiedGatewayManaged[T ownerMetaData](obj T, linkID string) bool {
230229
var metadata map[string]any
231230
switch o := any(obj).(type) {
232231
case *models.Frontend:
@@ -236,8 +235,28 @@ func isUnifiedGatewayManaged[T ownerMetaData](obj T) bool {
236235
default:
237236
return false
238237
}
239-
if _, ok := metadata[md.UnifiedGatewayMetaDataKey]; ok {
240-
return true
238+
hugVal, ok := metadata[md.UnifiedGatewayMetaDataKey]
239+
if !ok {
240+
return false
241+
}
242+
kindMap, ok := hugVal.(map[string]any)
243+
if !ok {
244+
return false
245+
}
246+
for _, objsAny := range kindMap {
247+
objMap, ok := objsAny.(map[string]any)
248+
if !ok {
249+
continue
250+
}
251+
for _, infoAny := range objMap {
252+
infoMap, ok := infoAny.(map[string]any)
253+
if !ok {
254+
continue
255+
}
256+
if id, ok := infoMap[md.LinkIDMetaDataKey].(string); ok && id == linkID {
257+
return true
258+
}
259+
}
241260
}
242261
return false
243262
}

hug/startup/startup_test.go

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
// Copyright 2025 HAProxy Technologies LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
package startup
15+
16+
import (
17+
"testing"
18+
19+
"github.com/haproxytech/client-native/v6/models"
20+
md "github.com/haproxytech/haproxy-unified-gateway/k8s/gate/haproxy/metadata"
21+
)
22+
23+
// hugMeta builds a metadata map matching the structure produced by the metadata
24+
// package after its internal JSON round-trip:
25+
// { "hug": { kind: { "ns/name": { "LinkID": linkID, "Generation": 1 } } } }
26+
func hugMeta(kind, objKey, linkID string) map[string]any {
27+
return map[string]any{
28+
md.UnifiedGatewayMetaDataKey: map[string]any{
29+
kind: map[string]any{
30+
objKey: map[string]any{
31+
"LinkID": linkID,
32+
"Generation": float64(1),
33+
},
34+
},
35+
},
36+
}
37+
}
38+
39+
func newFrontend(meta map[string]any) *models.Frontend {
40+
fe := &models.Frontend{}
41+
fe.Metadata = meta
42+
return fe
43+
}
44+
45+
func newBackend(meta map[string]any) *models.Backend {
46+
be := &models.Backend{}
47+
be.Metadata = meta
48+
return be
49+
}
50+
51+
func TestIsUnifiedGatewayManaged_Frontend(t *testing.T) {
52+
tests := []struct {
53+
name string
54+
frontend *models.Frontend
55+
linkID string
56+
want bool
57+
}{
58+
{
59+
name: "matching linkID",
60+
frontend: newFrontend(hugMeta("Gateway", "namespace/name", "hug")),
61+
linkID: "hug",
62+
want: true,
63+
},
64+
{
65+
name: "wrong linkID",
66+
frontend: newFrontend(hugMeta("Gateway", "namespace/name", "hug")),
67+
linkID: "other",
68+
want: false,
69+
},
70+
{
71+
name: "no hug key",
72+
frontend: newFrontend(map[string]any{"unrelated": "value"}),
73+
linkID: "hug",
74+
want: false,
75+
},
76+
{
77+
name: "nil metadata",
78+
frontend: newFrontend(nil),
79+
linkID: "hug",
80+
want: false,
81+
},
82+
{
83+
name: "empty metadata",
84+
frontend: newFrontend(map[string]any{}),
85+
linkID: "hug",
86+
want: false,
87+
},
88+
}
89+
for _, tt := range tests {
90+
t.Run(tt.name, func(t *testing.T) {
91+
got := isUnifiedGatewayManaged(tt.frontend, tt.linkID)
92+
if got != tt.want {
93+
t.Errorf("isUnifiedGatewayManaged() = %v, want %v", got, tt.want)
94+
}
95+
})
96+
}
97+
}
98+
99+
func TestIsUnifiedGatewayManaged_Backend(t *testing.T) {
100+
tests := []struct {
101+
name string
102+
backend *models.Backend
103+
linkID string
104+
want bool
105+
}{
106+
{
107+
name: "matching linkID",
108+
backend: newBackend(hugMeta("HTTPRoute", "namespace/name", "hug")),
109+
linkID: "hug",
110+
want: true,
111+
},
112+
{
113+
name: "wrong linkID",
114+
backend: newBackend(hugMeta("HTTPRoute", "namespace/name", "hug")),
115+
linkID: "other",
116+
want: false,
117+
},
118+
{
119+
name: "nil metadata",
120+
backend: newBackend(nil),
121+
linkID: "hug",
122+
want: false,
123+
},
124+
}
125+
for _, tt := range tests {
126+
t.Run(tt.name, func(t *testing.T) {
127+
got := isUnifiedGatewayManaged(tt.backend, tt.linkID)
128+
if got != tt.want {
129+
t.Errorf("isUnifiedGatewayManaged() = %v, want %v", got, tt.want)
130+
}
131+
})
132+
}
133+
}
134+
135+
func TestIsUnifiedGatewayManaged_MultipleObjects(t *testing.T) {
136+
// Two gateways under different object keys, both with the same linkID.
137+
meta := map[string]any{
138+
md.UnifiedGatewayMetaDataKey: map[string]any{
139+
"Gateway": map[string]any{
140+
"ns/gw-a": map[string]any{"LinkID": "hug", "Generation": float64(1)},
141+
"ns/gw-b": map[string]any{"LinkID": "hug", "Generation": float64(2)},
142+
},
143+
},
144+
}
145+
fe := newFrontend(meta)
146+
147+
if !isUnifiedGatewayManaged(fe, "hug") {
148+
t.Error("expected true for frontend with two gateways and matching linkID")
149+
}
150+
if isUnifiedGatewayManaged(fe, "other") {
151+
t.Error("expected false for frontend with two gateways and non-matching linkID")
152+
}
153+
}
154+
155+
func TestIsUnifiedGatewayManaged_MixedLinkIDs(t *testing.T) {
156+
// Two objects under different kinds, each with a different linkID.
157+
meta := map[string]any{
158+
md.UnifiedGatewayMetaDataKey: map[string]any{
159+
"Gateway": map[string]any{
160+
"ns/gw": map[string]any{"LinkID": "hug", "Generation": float64(1)},
161+
},
162+
"HTTPRoute": map[string]any{
163+
"ns/route": map[string]any{"LinkID": "other", "Generation": float64(1)},
164+
},
165+
},
166+
}
167+
fe := newFrontend(meta)
168+
169+
if !isUnifiedGatewayManaged(fe, "hug") {
170+
t.Error("expected true: Gateway entry has linkID 'hug'")
171+
}
172+
if !isUnifiedGatewayManaged(fe, "other") {
173+
t.Error("expected true: HTTPRoute entry has linkID 'other'")
174+
}
175+
if isUnifiedGatewayManaged(fe, "unknown") {
176+
t.Error("expected false: no entry has linkID 'unknown'")
177+
}
178+
}

k8s/gate/haproxy/metadata/metadata.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020

2121
const (
2222
UnifiedGatewayMetaDataKey string = "hug"
23+
LinkIDMetaDataKey string = "LinkID"
2324
)
2425

2526
type (

0 commit comments

Comments
 (0)