Skip to content

Commit d8f2f43

Browse files
committed
Add INI config utility to common/util module
This patch introduces a new INI manipulation utility with the ability to extend service configurations (customServiceConfig) by adding key-value pairs to specific sections. It includes comprehensive test coverage for various scenarios, including section detection, key validation, and edge cases. This utility does not replace existing keys if they are already specified via user input, nor does it add a new section if one does not already exist in the specified configuration. This design preserves the original purpose of customServiceConfig as user input. Related: https://issues.redhat.com/browse/OSPRH-14309 Signed-off-by: Francesco Pantano <[email protected]>
1 parent c83d830 commit d8f2f43

File tree

2 files changed

+279
-0
lines changed

2 files changed

+279
-0
lines changed

modules/common/util/ini.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
Copyright 2025 Red Hat
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 util
18+
19+
import (
20+
"errors"
21+
"fmt"
22+
"strings"
23+
)
24+
25+
type IniOption struct {
26+
Section string
27+
Key string
28+
Value string
29+
}
30+
31+
// Define static errors at package level
32+
var (
33+
ErrKeyAlreadyExists = errors.New("key already exists in section")
34+
ErrCouldNotPatchSection = errors.New("could not patch target section")
35+
)
36+
37+
// repr - print key: value in .ini format
38+
func (i *IniOption) repr() string {
39+
return fmt.Sprintf("%s = %s", i.Key, i.Value)
40+
}
41+
42+
// ExtendCustomServiceConfig - customServiceConfig is tokenized and parsed in a
43+
// loop where we keep track of two indexes:
44+
// - index: keep track of the current extracted token
45+
// - sectionIndex: when we detect a [<section>] within a token, we save the index
46+
// and we update it with the next section when is detected. This way we can
47+
// make sure to evaluate only the keys of the target section
48+
//
49+
// when an invalid case is detected, we return the customServiceConfig string
50+
// unchanged, otherwise the new key=value is appended as per the IniOption struct
51+
func ExtendCustomServiceConfig(
52+
iniString string,
53+
customServiceConfigExtend IniOption,
54+
) (string, error) {
55+
// customServiceConfig is empty
56+
if len(iniString) == 0 {
57+
return iniString, nil
58+
}
59+
// Position where insert new option (-1 = target section not found)
60+
index := -1
61+
// Current section header position (-1 = no section found)
62+
sectionIndex := -1
63+
svcConfigLines := strings.Split(iniString, "\n")
64+
sectionName := ""
65+
for idx, rawLine := range svcConfigLines {
66+
line := strings.TrimSpace(rawLine)
67+
token := strings.TrimSpace(strings.SplitN(line, "=", 2)[0])
68+
69+
if token == "" || strings.HasPrefix(token, "#") {
70+
// Skip blank lines and comments
71+
continue
72+
}
73+
if strings.HasPrefix(token, "[") && strings.HasSuffix(token, "]") {
74+
// Note the section name before looking for a backend_name
75+
sectionName = strings.Trim(token, "[]")
76+
sectionIndex = idx
77+
// increment the index (as an offset) only when a section is found
78+
if sectionName == customServiceConfigExtend.Section {
79+
index = idx + 1
80+
}
81+
}
82+
// Check if key already exists in target section
83+
if token == customServiceConfigExtend.Key && sectionIndex > -1 &&
84+
sectionName == customServiceConfigExtend.Section {
85+
errMsg := fmt.Errorf("%w: key %s in section %s", ErrKeyAlreadyExists, token, sectionName)
86+
return iniString, errMsg
87+
}
88+
}
89+
// index didn't progress during the customServiceConfig scan:
90+
// return unchanged, but no error
91+
if index == -1 {
92+
errMsg := fmt.Errorf("%w: %s", ErrCouldNotPatchSection, customServiceConfigExtend.Section)
93+
return iniString, errMsg
94+
}
95+
// index has a valid value and it is used as a pivot to inject the new ini
96+
// option right after the section
97+
var svcExtended []string
98+
svcExtended = append(svcExtended, svcConfigLines[:index]...)
99+
svcExtended = append(svcExtended, []string{customServiceConfigExtend.repr()}...)
100+
svcExtended = append(svcExtended, svcConfigLines[index:]...)
101+
return strings.Join(svcExtended, "\n"), nil
102+
}

modules/common/util/ini_test.go

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package util // nolint:revive
2+
3+
import (
4+
"testing"
5+
6+
"github.com/onsi/ginkgo/v2"
7+
. "github.com/onsi/gomega" // nolint:revive
8+
)
9+
10+
var defaultTestIniOption = IniOption{
11+
Section: "foo",
12+
Key: "s3_store_cacert",
13+
Value: "/etc/pki/tls/certs/ca-bundle.crt",
14+
}
15+
16+
var tests = []struct {
17+
name string
18+
input string
19+
option IniOption
20+
expected string
21+
err string
22+
}{
23+
{
24+
name: "empty customServiceConfig",
25+
input: "",
26+
option: defaultTestIniOption,
27+
expected: "",
28+
err: "",
29+
},
30+
{
31+
name: "field to non-existing section",
32+
input: `[DEFAULT]
33+
debug=true
34+
enabled_backends = backend:s3
35+
[foo]
36+
bar = bar
37+
foo = foo
38+
[backend]
39+
option1 = value1`,
40+
option: IniOption{
41+
Section: "bar",
42+
Key: "foo",
43+
Value: "foo",
44+
},
45+
expected: `[DEFAULT]
46+
debug=true
47+
enabled_backends = backend:s3
48+
[foo]
49+
bar = bar
50+
foo = foo
51+
[backend]
52+
option1 = value1`,
53+
err: "could not patch target section: bar",
54+
},
55+
{
56+
name: "add new ini line to a section in the middle",
57+
input: `[DEFAULT]
58+
debug=true
59+
enabled_backends = backend:s3
60+
[foo]
61+
bar = bar
62+
foo = foo
63+
[backend]
64+
option1 = value1`,
65+
option: defaultTestIniOption,
66+
expected: `[DEFAULT]
67+
debug=true
68+
enabled_backends = backend:s3
69+
[foo]
70+
s3_store_cacert = /etc/pki/tls/certs/ca-bundle.crt
71+
bar = bar
72+
foo = foo
73+
[backend]
74+
option1 = value1`,
75+
err: "",
76+
},
77+
{
78+
name: "section is not found, return it unchanged",
79+
input: `[DEFAULT]
80+
debug=true
81+
enabled_backends = backend:s3
82+
[backend]
83+
s3_store_cacert = /etc/pki/tls/certs/ca-bundle.crt `,
84+
option: defaultTestIniOption,
85+
expected: `[DEFAULT]
86+
debug=true
87+
enabled_backends = backend:s3
88+
[backend]
89+
s3_store_cacert = /etc/pki/tls/certs/ca-bundle.crt `,
90+
err: "could not patch target section: foo",
91+
},
92+
{
93+
name: "Add option to a section at the very beginning of customServiceConfig",
94+
input: `[foo]
95+
bar = bar
96+
foo = foo
97+
[backend]
98+
option1 = value1 `,
99+
option: defaultTestIniOption,
100+
expected: `[foo]
101+
s3_store_cacert = /etc/pki/tls/certs/ca-bundle.crt
102+
bar = bar
103+
foo = foo
104+
[backend]
105+
option1 = value1 `,
106+
err: "",
107+
},
108+
{
109+
name: "Add option to a section at the very bottom of customServiceConfig",
110+
input: `[DEFAULT]
111+
debug=true
112+
enabled_backends = backend:s3
113+
[backend]
114+
# this is a comment
115+
option1 = value1
116+
[foo]
117+
# this is a comment
118+
bar = bar
119+
foo = foo`,
120+
option: defaultTestIniOption,
121+
expected: `[DEFAULT]
122+
debug=true
123+
enabled_backends = backend:s3
124+
[backend]
125+
# this is a comment
126+
option1 = value1
127+
[foo]
128+
s3_store_cacert = /etc/pki/tls/certs/ca-bundle.crt
129+
# this is a comment
130+
bar = bar
131+
foo = foo`,
132+
err: "",
133+
},
134+
{
135+
name: "Add option to an empty target section",
136+
input: `[DEFAULT]
137+
debug=true
138+
[foo]`,
139+
option: defaultTestIniOption,
140+
expected: `[DEFAULT]
141+
debug=true
142+
[foo]
143+
s3_store_cacert = /etc/pki/tls/certs/ca-bundle.crt`,
144+
err: "",
145+
},
146+
{
147+
name: "key/value already present in the target section",
148+
input: `[DEFAULT]
149+
debug=true
150+
[foo]
151+
s3_store_cacert = /my/custom/path/ca-bundle.crt`,
152+
option: defaultTestIniOption,
153+
expected: `[DEFAULT]
154+
debug=true
155+
[foo]
156+
s3_store_cacert = /my/custom/path/ca-bundle.crt`,
157+
err: "key already exists in section: key s3_store_cacert in section foo",
158+
},
159+
}
160+
161+
func TestExtendCustomServiceConfig(t *testing.T) {
162+
for _, tt := range tests {
163+
t.Run(tt.name, func(t *testing.T) {
164+
g := NewWithT(t)
165+
output, err := ExtendCustomServiceConfig(tt.input, tt.option)
166+
g.Expect(output).To(Equal(tt.expected))
167+
if err != nil {
168+
// check the string matches the expected error message
169+
ginkgo.GinkgoWriter.Println(err.Error())
170+
ginkgo.GinkgoWriter.Println(tt.err)
171+
g.Expect(err.Error()).To(Equal(tt.err))
172+
} else {
173+
g.Expect(err).ToNot(HaveOccurred())
174+
}
175+
})
176+
}
177+
}

0 commit comments

Comments
 (0)