Skip to content

Commit 4820e8f

Browse files
committed
BUG/MEDIUM: crt-store: do not write empty crt-base and key-base
SerializeCrtStore always set crt-base and key-base, so a store without them rendered bare keywords that HAProxy rejects with "'crt-base' requires a <path> argument", and no crt-store without both bases could be deployed. Set the attribute only when the value is non-empty and remove it otherwise. As a safety net, simple.Word no longer renders an empty value: Parse refuses a bare keyword, so Result now skips it as well instead of writing a line that cannot be read back.
1 parent b3eb747 commit 4820e8f

5 files changed

Lines changed: 152 additions & 9 deletions

File tree

config-parser/parsers/simple/simple-word.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ func (s *Word) Parse(line string, parts []string, comment string) (string, error
4545
}
4646

4747
func (s *Word) Result() ([]common.ReturnResultLine, error) {
48-
if s.data == nil {
48+
// a bare keyword is never parsed, so it is never written either
49+
if s.data == nil || s.data.Value == "" {
4950
return nil, errors.ErrFetch
5051
}
5152
return []common.ReturnResultLine{
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
Copyright 2026 HAProxy Technologies
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package tests
18+
19+
import (
20+
"errors"
21+
"testing"
22+
23+
parsererrors "github.com/haproxytech/client-native/v6/config-parser/errors"
24+
"github.com/haproxytech/client-native/v6/config-parser/parsers/simple"
25+
"github.com/haproxytech/client-native/v6/config-parser/types"
26+
)
27+
28+
// A Word never parses a bare keyword, so it must not write one either: an
29+
// empty value renders nothing instead of a line HAProxy rejects.
30+
func TestWordEmptyValueIsNotWritten(t *testing.T) {
31+
parser := &simple.Word{Name: "crt-base"}
32+
parser.Init()
33+
34+
if err := parser.Set(types.StringC{Value: ""}, -1); err != nil {
35+
t.Fatal(err)
36+
}
37+
if _, err := parser.Result(); !errors.Is(err, parsererrors.ErrFetch) {
38+
t.Errorf("empty value: got %v, want ErrFetch", err)
39+
}
40+
41+
if err := parser.Set(types.StringC{Value: "/certs"}, -1); err != nil {
42+
t.Fatal(err)
43+
}
44+
result, err := parser.Result()
45+
if err != nil {
46+
t.Fatal(err)
47+
}
48+
if len(result) != 1 || result[0].Data != "crt-base /certs" {
49+
t.Errorf("got %+v, want [crt-base /certs]", result)
50+
}
51+
}

configuration/crt_store.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -216,15 +216,16 @@ func SerializeCrtStore(p parser.Parser, store *models.CrtStore) error {
216216
}
217217
}
218218

219-
crtBase := types.StringC{Value: store.CrtBase}
220-
if err := p.Set(parser.CrtStore, store.Name, "crt-base", crtBase); err != nil {
221-
return err
219+
// crt-base and key-base take a mandatory path: an empty value removes
220+
// the line instead of writing a bare keyword HAProxy rejects
221+
setBase := func(attribute, value string) error {
222+
if value == "" {
223+
return p.Set(parser.CrtStore, store.Name, attribute, nil)
224+
}
225+
return p.Set(parser.CrtStore, store.Name, attribute, types.StringC{Value: value})
222226
}
223-
224-
keyBase := types.StringC{Value: store.KeyBase}
225-
if err := p.Set(parser.CrtStore, store.Name, "key-base", keyBase); err != nil {
227+
if err := setBase("crt-base", store.CrtBase); err != nil {
226228
return err
227229
}
228-
229-
return nil
230+
return setBase("key-base", store.KeyBase)
230231
}

configuration/crt_store_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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 configuration
17+
18+
import (
19+
"strings"
20+
"testing"
21+
22+
"github.com/stretchr/testify/require"
23+
24+
parser "github.com/haproxytech/client-native/v6/config-parser"
25+
"github.com/haproxytech/client-native/v6/config-parser/options"
26+
"github.com/haproxytech/client-native/v6/models"
27+
)
28+
29+
func renderCrtStore(t *testing.T, config string, store *models.CrtStore) string {
30+
t.Helper()
31+
p, err := parser.New(options.String(config))
32+
require.NoError(t, err)
33+
require.NoError(t, SerializeCrtStore(p, store))
34+
return strings.TrimSpace(p.String())
35+
}
36+
37+
// crt-base and key-base take a mandatory path: a store without them must
38+
// render a bare section, not empty keywords that HAProxy rejects.
39+
func TestSerializeCrtStoreWithoutBases(t *testing.T) {
40+
store := &models.CrtStore{CrtStoreBase: models.CrtStoreBase{Name: "cs"}}
41+
require.Equal(t, "crt-store cs", renderCrtStore(t, "crt-store cs\n", store))
42+
}
43+
44+
// Clearing a base on edit removes its line instead of leaving the keyword.
45+
func TestSerializeCrtStoreClearsBases(t *testing.T) {
46+
store := &models.CrtStore{CrtStoreBase: models.CrtStoreBase{Name: "cs", CrtBase: "/certs"}}
47+
out := renderCrtStore(t, "crt-store cs\n crt-base /old\n key-base /keys\n", store)
48+
require.Equal(t, "crt-store cs\n crt-base /certs", out)
49+
}
50+
51+
func TestSerializeCrtStoreRoundTrip(t *testing.T) {
52+
store := &models.CrtStore{CrtStoreBase: models.CrtStoreBase{Name: "cs", CrtBase: "/certs", KeyBase: "/keys"}}
53+
out := renderCrtStore(t, "crt-store cs\n", store)
54+
55+
p, err := parser.New(options.String(out))
56+
require.NoError(t, err)
57+
got := &models.CrtStore{CrtStoreBase: models.CrtStoreBase{Name: "cs"}}
58+
require.NoError(t, ParseCrtStore(p, got))
59+
require.True(t, got.CrtStoreBase.Equal(store.CrtStoreBase))
60+
}

test/crt_store_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,3 +301,33 @@ func TestCreateEditDeleteCrtLoads(t *testing.T) {
301301
t.Fatal("DeleteCrtLoad() did not work correctly")
302302
}
303303
}
304+
305+
// crt-base and key-base are optional: a store created without them must be
306+
// readable back as is, and clearing a base on edit must drop its line.
307+
func TestCreateEditCrtStoreWithoutBases(t *testing.T) {
308+
name := fmt.Sprintf("test-store-nobase-%d", version)
309+
store := &models.CrtStore{CrtStoreBase: models.CrtStoreBase{Name: name}}
310+
311+
require.NoError(t, clientTest.CreateCrtStore(store, "", version))
312+
version++
313+
314+
_, created, err := clientTest.GetCrtStore(name, "")
315+
require.NoError(t, err)
316+
require.True(t, created.Equal(*store), "diff %v", cmp.Diff(*created, *store))
317+
318+
store.CrtBase = "/secure/certs"
319+
require.NoError(t, clientTest.EditCrtStore(name, store, "", version))
320+
version++
321+
322+
store.CrtBase = ""
323+
store.KeyBase = "/secure/keys"
324+
require.NoError(t, clientTest.EditCrtStore(name, store, "", version))
325+
version++
326+
327+
_, edited, err := clientTest.GetCrtStore(name, "")
328+
require.NoError(t, err)
329+
require.True(t, edited.Equal(*store), "diff %v", cmp.Diff(*edited, *store))
330+
331+
require.NoError(t, clientTest.DeleteCrtStore(name, "", version))
332+
version++
333+
}

0 commit comments

Comments
 (0)