Skip to content

Commit 177b9a1

Browse files
committed
BUG/MEDIUM: crt-store: key structured crt loads by certificate
The structured crt_store built its crt_loads map with NamedResourceArrayToMap, which looks up a Name field. CrtLoad is identified by its Certificate, so any crt-store containing a load line made the structured GET fail with "object does not have an exportable 'Name' field". Expose the StructuredCrtStore interface through the Structured interface, it was never reachable from the Configuration client.
1 parent a7e3a2f commit 177b9a1

6 files changed

Lines changed: 122 additions & 3 deletions

File tree

configuration/structured.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type Structured interface {
3636
StructuredTraces
3737
StructuredAcmeProvider
3838
StructuredHealthcheck
39+
StructuredCrtStore
3940
}
4041

4142
type StructuredToParserArgs struct {

configuration/structured_crt_store.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ func parseCrtStoreSection(name string, p parser.Parser) (*models.CrtStore, error
183183
if err != nil {
184184
return nil, err
185185
}
186-
crtLoads, err := convert.NamedResourceArrayToMap(loads)
186+
crtLoads, err := convert.NamedResourceArrayToMapWithKey(loads, "Certificate")
187187
if err != nil {
188188
return nil, err
189189
}

test/configuration_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,6 +1064,8 @@ acme test
10641064
crt-store cert-bunker1
10651065
crt-base /secure/certs
10661066
key-base /secure/keys
1067+
load crt example.com.pem alias example
1068+
load crt lol.pem key lol.key
10671069
10681070
traces
10691071
trace h1 sink buf1 level developer verbosity complete start now

test/crt_store_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ func checkCrtStores(t *testing.T, got map[string]models.CrtStores) {
5050
for _, g := range v {
5151
for _, w := range want {
5252
if g.Name == w.Name {
53-
require.True(t, g.Equal(*w), "k=%s - diff %v", k, cmp.Diff(*g, *w))
53+
// non structured GET does not return child resources
54+
require.True(t, g.CrtStoreBase.Equal(w.CrtStoreBase), "k=%s - diff %v", k, cmp.Diff(*g, *w))
5455
break
5556
}
5657
}

test/expected/structured.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3342,7 +3342,17 @@
33423342
"cert-bunker1": {
33433343
"name": "cert-bunker1",
33443344
"crt_base": "/secure/certs",
3345-
"key_base": "/secure/keys"
3345+
"key_base": "/secure/keys",
3346+
"crt_loads": {
3347+
"example.com.pem": {
3348+
"certificate": "example.com.pem",
3349+
"alias": "example"
3350+
},
3351+
"lol.pem": {
3352+
"certificate": "lol.pem",
3353+
"key": "lol.key"
3354+
}
3355+
}
33463356
}
33473357
},
33483358
"traces": {

test/structured_crt_store_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright 2026 HAProxy Technologies
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
16+
package test
17+
18+
import (
19+
"os"
20+
"testing"
21+
22+
"github.com/google/go-cmp/cmp"
23+
"github.com/haproxytech/client-native/v6/models"
24+
"github.com/stretchr/testify/require"
25+
)
26+
27+
func checkStructuredCrtStores(t *testing.T, got map[string]models.CrtStores) {
28+
exp := crtStoreExpectations()
29+
for k, v := range got {
30+
want, ok := exp[k]
31+
require.True(t, ok, "k=%s", k)
32+
require.Equal(t, len(want), len(v), "k=%s", k)
33+
for _, g := range v {
34+
for _, w := range want {
35+
if g.Name == w.Name {
36+
require.True(t, g.Equal(*w), "k=%s - diff %v", k, cmp.Diff(*g, *w))
37+
break
38+
}
39+
}
40+
}
41+
}
42+
}
43+
44+
func TestGetStructuredCrtStores(t *testing.T) {
45+
clientTest, filename, err := getTestClient()
46+
require.NoError(t, err)
47+
defer os.Remove(filename)
48+
version := int64(1)
49+
50+
v, stores, err := clientTest.GetStructuredCrtStores("")
51+
require.NoError(t, err)
52+
require.Equal(t, version, v, "Version %v returned, expected %v", v, version)
53+
require.Len(t, stores, 1)
54+
checkStructuredCrtStores(t, map[string]models.CrtStores{"": stores})
55+
}
56+
57+
func TestGetStructuredCrtStore(t *testing.T) {
58+
clientTest, filename, err := getTestClient()
59+
require.NoError(t, err)
60+
defer os.Remove(filename)
61+
version := int64(1)
62+
63+
v, store, err := clientTest.GetStructuredCrtStore(testCrtStoreName, "")
64+
require.NoError(t, err)
65+
require.Equal(t, version, v, "Version %v returned, expected %v", v, version)
66+
require.Len(t, store.CrtLoads, 2)
67+
checkStructuredCrtStores(t, map[string]models.CrtStores{testCrtStoreName: {store}})
68+
69+
_, _, err = clientTest.GetStructuredCrtStore("doesnotexist", "")
70+
require.Error(t, err, "should throw error, non existent crt_store")
71+
}
72+
73+
func TestCreateEditStructuredCrtStore(t *testing.T) {
74+
clientTest, filename, err := getTestClient()
75+
require.NoError(t, err)
76+
defer os.Remove(filename)
77+
version := int64(1)
78+
79+
store := &models.CrtStore{
80+
CrtStoreBase: models.CrtStoreBase{Name: "test-structured", CrtBase: "/certs", KeyBase: "/keys"},
81+
CrtLoads: map[string]models.CrtLoad{
82+
"c1.pem": {Certificate: "c1.pem", Key: "k1.pem", Alias: "c1"},
83+
"c2.pem": {Certificate: "c2.pem", Ocsp: "ocsp.der", OcspUpdate: models.CrtLoadOcspUpdateEnabled},
84+
},
85+
}
86+
require.NoError(t, clientTest.CreateStructuredCrtStore(store, "", version))
87+
version++
88+
89+
v, got, err := clientTest.GetStructuredCrtStore("test-structured", "")
90+
require.NoError(t, err)
91+
require.Equal(t, version, v, "Version %v returned, expected %v", v, version)
92+
require.True(t, got.Equal(*store), "diff %v", cmp.Diff(*got, *store))
93+
94+
require.Error(t, clientTest.CreateStructuredCrtStore(store, "", version), "should throw error, crt_store already exists")
95+
96+
// edit: keep a single, different load entry
97+
store.CrtLoads = map[string]models.CrtLoad{"c3.pem": {Certificate: "c3.pem"}}
98+
require.NoError(t, clientTest.EditStructuredCrtStore("test-structured", store, "", version))
99+
version++
100+
101+
v, got, err = clientTest.GetStructuredCrtStore("test-structured", "")
102+
require.NoError(t, err)
103+
require.Equal(t, version, v, "Version %v returned, expected %v", v, version)
104+
require.True(t, got.Equal(*store), "diff %v", cmp.Diff(*got, *store))
105+
}

0 commit comments

Comments
 (0)