Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions modules/common/util/ini.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
Copyright 2025 Red Hat

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package util

import (
"errors"
"fmt"
"strings"
)

// IniOption -
type IniOption struct {
Section string
Key string
Value string
Unique bool
}

// Define static errors
var (
ErrKeyAlreadyExists = errors.New("key already exists in section")
ErrCouldNotPatchSection = errors.New("could not patch target section")
)

// repr - print key: value in .ini format
func (i *IniOption) repr() string {
return fmt.Sprintf("%s = %s", i.Key, i.Value)
}

// ExtendCustomServiceConfig - customServiceConfig is tokenized and parsed in a
// loop where we keep track of two indexes:
// - index: keep track of the current extracted token
// - sectionIndex: when we detect a [<section>] within a token, we save the index
// and we update it with the next section when is detected. This way we can
// make sure to evaluate only the keys of the target section
//
// when an invalid case is detected, we return the customServiceConfig string
// unchanged, otherwise the new key=value is appended as per the IniOption struct
func ExtendCustomServiceConfig(
iniString string,
customServiceConfigExtend IniOption,
) (string, error) {
// customServiceConfig is empty
if len(iniString) == 0 {
return iniString, nil
}
// Position where insert new option (-1 = target section not found)
index := -1
// Current section header position (-1 = no section found)
sectionIndex := -1
svcConfigLines := strings.Split(iniString, "\n")
sectionName := ""
for idx, rawLine := range svcConfigLines {
line := strings.TrimSpace(rawLine)
token := strings.TrimSpace(strings.SplitN(line, "=", 2)[0])

if token == "" || strings.HasPrefix(token, "#") {
// Skip blank lines and comments
continue
}
if strings.HasPrefix(token, "[") && strings.HasSuffix(token, "]") {
// Note the section name before looking for a backend_name
sectionName = strings.Trim(token, "[]")
sectionIndex = idx
// increment the index (as an offset) only when a section is found
if sectionName == customServiceConfigExtend.Section {
index = idx + 1
}
}
// Check if key already exists in target section
if customServiceConfigExtend.Unique && token == customServiceConfigExtend.Key && sectionIndex > -1 &&
sectionName == customServiceConfigExtend.Section {
errMsg := fmt.Errorf("%w: key %s in section %s", ErrKeyAlreadyExists, token, sectionName)
return iniString, errMsg
}
}
// index didn't progress during the customServiceConfig scan:
// return unchanged, but no error
if index == -1 {
errMsg := fmt.Errorf("%w: %s", ErrCouldNotPatchSection, customServiceConfigExtend.Section)
return iniString, errMsg
}
// index has a valid value and it is used as a pivot to inject the new ini
// option right after the section
var svcExtended []string
svcExtended = append(svcExtended, svcConfigLines[:index]...)
svcExtended = append(svcExtended, []string{customServiceConfigExtend.repr()}...)
svcExtended = append(svcExtended, svcConfigLines[index:]...)
return strings.Join(svcExtended, "\n"), nil
}
221 changes: 221 additions & 0 deletions modules/common/util/ini_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
package util // nolint:revive

import (
"testing"

. "github.com/onsi/gomega" // nolint:revive
)

var (
defaultTestIniOption = IniOption{
Section: "foo",
Key: "s3_store_cacert",
Value: "/etc/pki/tls/certs/ca-bundle.crt",
Unique: true,
}

aliasKeyIniOption = IniOption{
Section: "pci",
Key: "alias",
Value: "{ \"device_type\": \"type-VF\", \"resource_class\": \"CUSTOM_A16_16A\", \"name\": \"A16_16A\" }",
Unique: false,
}

deviceSpecIniOption = IniOption{
Section: "pci",
Key: "device_spec",
Value: "{ \"vendor_id\": \"10de\", \"product_id\": \"25b6\", \"address\": \"0000:25:00.6\", \"resource_class\": \"CUSTOM_A16_8A\", \"managed\": \"yes\" }",
Unique: false,
}
)

var tests = []struct {
name string
input string
option IniOption
expected string
err string
}{
{
name: "empty customServiceConfig",
input: "",
option: defaultTestIniOption,
expected: "",
err: "",
},
{
name: "field to non-existing section",
input: `[DEFAULT]
debug=true
enabled_backends = backend:s3
[foo]
bar = bar
foo = foo
[backend]
option1 = value1`,
option: IniOption{
Section: "bar",
Key: "foo",
Value: "foo",
},
expected: `[DEFAULT]
debug=true
enabled_backends = backend:s3
[foo]
bar = bar
foo = foo
[backend]
option1 = value1`,
err: "could not patch target section: bar",
},
{
name: "add new ini line to a section in the middle",
input: `[DEFAULT]
debug=true
enabled_backends = backend:s3
[foo]
bar = bar
foo = foo
[backend]
option1 = value1`,
option: defaultTestIniOption,
expected: `[DEFAULT]
debug=true
enabled_backends = backend:s3
[foo]
s3_store_cacert = /etc/pki/tls/certs/ca-bundle.crt
bar = bar
foo = foo
[backend]
option1 = value1`,
err: "",
},
{
name: "section is not found, return it unchanged",
input: `[DEFAULT]
debug=true
enabled_backends = backend:s3
[backend]
s3_store_cacert = /etc/pki/tls/certs/ca-bundle.crt `,
option: defaultTestIniOption,
expected: `[DEFAULT]
debug=true
enabled_backends = backend:s3
[backend]
s3_store_cacert = /etc/pki/tls/certs/ca-bundle.crt `,
err: "could not patch target section: foo",
},
{
name: "Add option to a section at the very beginning of customServiceConfig",
input: `[foo]
bar = bar
foo = foo
[backend]
option1 = value1 `,
option: defaultTestIniOption,
expected: `[foo]
s3_store_cacert = /etc/pki/tls/certs/ca-bundle.crt
bar = bar
foo = foo
[backend]
option1 = value1 `,
err: "",
},
{
name: "Add option to a section at the very bottom of customServiceConfig",
input: `[DEFAULT]
debug=true
enabled_backends = backend:s3
[backend]
# this is a comment
option1 = value1
[foo]
# this is a comment
bar = bar
foo = foo`,
option: defaultTestIniOption,
expected: `[DEFAULT]
debug=true
enabled_backends = backend:s3
[backend]
# this is a comment
option1 = value1
[foo]
s3_store_cacert = /etc/pki/tls/certs/ca-bundle.crt
# this is a comment
bar = bar
foo = foo`,
err: "",
},
{
name: "Add option to an empty target section",
input: `[DEFAULT]
debug=true
[foo]`,
option: defaultTestIniOption,
expected: `[DEFAULT]
debug=true
[foo]
s3_store_cacert = /etc/pki/tls/certs/ca-bundle.crt`,
err: "",
},
{
name: "key/value already present in the target section",
input: `[DEFAULT]
debug=true
[foo]
s3_store_cacert = /my/custom/path/ca-bundle.crt`,
option: defaultTestIniOption,
expected: `[DEFAULT]
debug=true
[foo]
s3_store_cacert = /my/custom/path/ca-bundle.crt`,
err: "key already exists in section: key s3_store_cacert in section foo",
},
{
name: "add new ini line anyway even though a section contains the same key ",
input: `[pci]
device_spec = { "vendor_id": "10de", "product_id": "25b6", "address": "0000:25:00.4", "resource_class": "CUSTOM_A16_16A", "managed": "no" }
device_spec = { "vendor_id": "10de", "product_id": "25b6", "address": "0000:25:00.5", "resource_class": "CUSTOM_A16_8A", "managed": "no" }
alias = { "device_type": "type-VF", "resource_class": "CUSTOM_A16_8A", "name": "A16_8A" }`,
option: aliasKeyIniOption,
expected: `[pci]
alias = { "device_type": "type-VF", "resource_class": "CUSTOM_A16_16A", "name": "A16_16A" }
device_spec = { "vendor_id": "10de", "product_id": "25b6", "address": "0000:25:00.4", "resource_class": "CUSTOM_A16_16A", "managed": "no" }
device_spec = { "vendor_id": "10de", "product_id": "25b6", "address": "0000:25:00.5", "resource_class": "CUSTOM_A16_8A", "managed": "no" }
alias = { "device_type": "type-VF", "resource_class": "CUSTOM_A16_8A", "name": "A16_8A" }`,
err: "",
},
{
name: "add new ini line anyway even though a section contains the same key ",
input: `[pci]
device_spec = { "vendor_id": "10de", "product_id": "25b6", "address": "0000:25:00.4", "resource_class": "CUSTOM_A16_16A", "managed": "no" }
device_spec = { "vendor_id": "10de", "product_id": "25b6", "address": "0000:25:00.5", "resource_class": "CUSTOM_A16_8A", "managed": "no" }
alias = { "device_type": "type-VF", "resource_class": "CUSTOM_A16_16A", "name": "A16_16A" }
alias = { "device_type": "type-VF", "resource_class": "CUSTOM_A16_8A", "name": "A16_8A" }`,
option: deviceSpecIniOption,
expected: `[pci]
device_spec = { "vendor_id": "10de", "product_id": "25b6", "address": "0000:25:00.6", "resource_class": "CUSTOM_A16_8A", "managed": "yes" }
device_spec = { "vendor_id": "10de", "product_id": "25b6", "address": "0000:25:00.4", "resource_class": "CUSTOM_A16_16A", "managed": "no" }
device_spec = { "vendor_id": "10de", "product_id": "25b6", "address": "0000:25:00.5", "resource_class": "CUSTOM_A16_8A", "managed": "no" }
alias = { "device_type": "type-VF", "resource_class": "CUSTOM_A16_16A", "name": "A16_16A" }
alias = { "device_type": "type-VF", "resource_class": "CUSTOM_A16_8A", "name": "A16_8A" }`,
err: "",
},
}

func TestExtendCustomServiceConfig(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
output, err := ExtendCustomServiceConfig(tt.input, tt.option)
g.Expect(output).To(Equal(tt.expected))
if err != nil {
// check the string matches the expected error message
g.Expect(err.Error()).To(Equal(tt.err))
} else {
g.Expect(err).ToNot(HaveOccurred())
}
})
}
}