Skip to content

Commit 0e2aefd

Browse files
authored
Merge pull request #611 from Luap99/configfile
storage: add new pkg/configfile package
2 parents 28c83ab + 3a7e77d commit 0e2aefd

7 files changed

Lines changed: 1177 additions & 0 deletions

File tree

storage/pkg/configfile/doc.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Package configfile provides the utilities for our config file parsing.
2+
//
3+
// Note the API here is not considered stable and can and will change and we see fit.
4+
// The purpose is to use this only for our own config file parsing such as
5+
// containers.conf, storage.conf and registries.conf. We will not consider use cases
6+
// for external consumers.
7+
package configfile

storage/pkg/configfile/parse.go

Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
package configfile
2+
3+
import (
4+
"errors"
5+
"io"
6+
"io/fs"
7+
"iter"
8+
"maps"
9+
"os"
10+
"path/filepath"
11+
"slices"
12+
"strconv"
13+
"strings"
14+
15+
"github.com/BurntSushi/toml"
16+
"github.com/sirupsen/logrus"
17+
)
18+
19+
const _configPathName = "containers"
20+
21+
var (
22+
// systemConfigPath is the location for the default config files shipped by the distro/vendor.
23+
//
24+
// This can be overridden at build time with the following go linker flag:
25+
// -ldflags '-X go.podman.io/storage/pkg/configfile.systemConfigPath=$your_path'
26+
systemConfigPath = builtinSystemConfigPath
27+
28+
// adminOverrideConfigPath is the location for admin local override config files.
29+
//
30+
// This can be overridden at build time with the following go linker flag:
31+
// -ldflags '-X go.podman.io/storage/pkg/configfile.adminOverrideConfigPath=$your_path'
32+
adminOverrideConfigPath = getAdminOverrideConfigPath()
33+
)
34+
35+
type File struct {
36+
// The name of the config file WITHOUT the extension (i.e. no .conf).
37+
// Must not be empty and must not contain the path separator.
38+
Name string
39+
40+
// Extension is the file extension of the config file, i.e. "conf" or "yaml".
41+
// Must not be empty and must not contain the path separator.
42+
Extension string
43+
44+
// EnvironmentName is the name of environment variable that can be set to specify the override.
45+
// Optional.
46+
EnvironmentName string
47+
48+
// RootForImplicitAbsolutePaths is the path to an alternate root
49+
// If not "", prefixed to any absolute paths used by default in the package.
50+
// NOTE: This does NOT affect paths starting by $HOME or environment variables paths.
51+
RootForImplicitAbsolutePaths string
52+
53+
// DoNotLoadMainFiles should be set if only the Drop In files should be loaded.
54+
DoNotLoadMainFiles bool
55+
56+
// DoNotLoadDropInFiles should be set if only the main files should be loaded.
57+
DoNotLoadDropInFiles bool
58+
59+
// DoNotUseExtensionForConfigName makes it so that the extension is only consulted for the drop in
60+
// file names but not the main config file name search path.
61+
DoNotUseExtensionForConfigName bool
62+
63+
// UserId is the id of the user running this. Used to know where to search in the
64+
// different "rootful" and "rootless" drop in lookup paths.
65+
UserId int
66+
67+
// Modules is a list of names of full paths which are loaded after all the other files.
68+
// Note the modules concept exists only for containers.conf.
69+
// For compatibility reasons this field is written to with the fully resolved paths
70+
// of each module as this is what podman expects today.
71+
Modules []string
72+
}
73+
74+
// Item is a single config file that is being read once at a time and returned by the iterator from [Read].
75+
type Item struct {
76+
// Reader is the reader from the file content. The Reader is only valid during
77+
Reader io.Reader
78+
// Name is the full filepath to the filename being read.
79+
Name string
80+
}
81+
82+
func getConfName(name, extension string, noExtension bool) string {
83+
if noExtension {
84+
return name
85+
}
86+
return name + "." + extension
87+
}
88+
89+
// Read parses all config files with the specified options and returns an iterator which returns all files as Item in the right order.
90+
// If an error is returned by the iterator then this must be treated as fatal error and must fail the config file parsing.
91+
// Expected ENOENT errors are already ignored in this function and must not be handled again by callers.
92+
// The given File options must not be nil and populated with valid options.
93+
func Read(conf *File) iter.Seq2[*Item, error] {
94+
configFileName := getConfName(conf.Name, conf.Extension, conf.DoNotUseExtensionForConfigName)
95+
96+
// Note this can be empty which is a valid case and should be simply ignored then.
97+
defaultConfig := systemConfigPath
98+
if defaultConfig != "" {
99+
defaultConfig = filepath.Join(defaultConfig, configFileName)
100+
if conf.RootForImplicitAbsolutePaths != "" {
101+
defaultConfig = filepath.Join(conf.RootForImplicitAbsolutePaths, defaultConfig)
102+
}
103+
}
104+
105+
// Same here this can be empty.
106+
overrideConfig := adminOverrideConfigPath
107+
if overrideConfig != "" {
108+
overrideConfig = filepath.Join(overrideConfig, configFileName)
109+
if conf.RootForImplicitAbsolutePaths != "" {
110+
overrideConfig = filepath.Join(conf.RootForImplicitAbsolutePaths, overrideConfig)
111+
}
112+
}
113+
114+
return func(yield func(*Item, error) bool) {
115+
shouldLoadMainFile := !conf.DoNotLoadMainFiles
116+
shouldLoadDropIns := !conf.DoNotLoadDropInFiles
117+
118+
yieldAndClose := func(f *os.File) bool {
119+
ok := yield(&Item{
120+
Reader: f,
121+
Name: f.Name(),
122+
}, nil)
123+
// Once yield returns always close the file as the consumer should be done with it.
124+
if err := f.Close(); err != nil {
125+
if ok {
126+
// don't yield again if the previous yield returned false
127+
yield(nil, err)
128+
}
129+
return false
130+
}
131+
return ok
132+
}
133+
134+
if conf.EnvironmentName != "" {
135+
if path := os.Getenv(conf.EnvironmentName); path != "" {
136+
f, err := os.Open(path)
137+
// Do not ignore ErrNotExist here, we want to hard error if users set a wrong path here.
138+
if err != nil {
139+
yield(nil, err)
140+
return
141+
}
142+
if !yieldAndClose(f) {
143+
return
144+
}
145+
// Also when the env is set skip the loading of the main and drop in files, modules and _OVERRIDE env are still read though.
146+
shouldLoadMainFile = false
147+
shouldLoadDropIns = false
148+
}
149+
}
150+
151+
// userConfig can be empty as well
152+
userConfig, err := UserConfigPath()
153+
if err != nil {
154+
// return error via iterator
155+
yield(nil, err)
156+
return
157+
}
158+
if userConfig != "" {
159+
userConfig = filepath.Join(userConfig, configFileName)
160+
}
161+
162+
if shouldLoadMainFile {
163+
for _, path := range []string{userConfig, overrideConfig, defaultConfig} {
164+
if path == "" {
165+
continue
166+
}
167+
f, err := os.Open(path)
168+
// only ignore ErrNotExist, all other errors get return to the caller via yield
169+
if err != nil {
170+
if errors.Is(err, fs.ErrNotExist) {
171+
continue
172+
}
173+
yield(nil, err)
174+
return
175+
}
176+
177+
if !yieldAndClose(f) {
178+
return
179+
}
180+
// we only read the first file
181+
break
182+
}
183+
}
184+
185+
if shouldLoadDropIns {
186+
files, err := readDropIns(defaultConfig, overrideConfig, userConfig, conf.Extension, conf.UserId)
187+
if err != nil {
188+
// return error via iterator
189+
yield(nil, err)
190+
return
191+
}
192+
for _, file := range files {
193+
f, err := os.Open(file)
194+
// only ignore ErrNotExist, all other errors get return to the caller via yield
195+
if err != nil {
196+
if errors.Is(err, fs.ErrNotExist) {
197+
continue
198+
}
199+
yield(nil, err)
200+
return
201+
}
202+
203+
if !yieldAndClose(f) {
204+
return
205+
}
206+
}
207+
}
208+
209+
if len(conf.Modules) > 0 {
210+
dirs := moduleDirectories(defaultConfig, overrideConfig, userConfig)
211+
resolvedModules := make([]string, 0, len(conf.Modules))
212+
for _, module := range conf.Modules {
213+
f, err := resolveModule(module, dirs)
214+
if err != nil {
215+
yield(nil, err)
216+
return
217+
}
218+
resolvedModules = append(resolvedModules, f.Name())
219+
if !yieldAndClose(f) {
220+
return
221+
}
222+
}
223+
conf.Modules = resolvedModules
224+
}
225+
226+
if conf.EnvironmentName != "" {
227+
// The _OVERRIDE env must be appended after loading all files, even modules.
228+
if path := os.Getenv(conf.EnvironmentName + "_OVERRIDE"); path != "" {
229+
f, err := os.Open(path)
230+
// Do not ignore ErrNotExist here, we want to hard error if users set a wrong path here.
231+
if err != nil {
232+
yield(nil, err)
233+
return
234+
}
235+
if !yieldAndClose(f) {
236+
return
237+
}
238+
}
239+
}
240+
}
241+
}
242+
243+
const dropInSuffix = ".d"
244+
245+
func readDropIns(defaultConfig, overrideConfig, userConfig, extension string, uid int) ([]string, error) {
246+
dropInMap := make(map[string]string)
247+
paths := make([]string, 0, 7)
248+
249+
suffix := "." + extension
250+
251+
if defaultConfig != "" {
252+
paths = append(paths, getDropInPaths(defaultConfig, suffix, uid)...)
253+
}
254+
if overrideConfig != "" {
255+
paths = append(paths, getDropInPaths(overrideConfig, suffix, uid)...)
256+
}
257+
if userConfig != "" {
258+
// the $HOME config only has one .d path not the rootful/rootless ones.
259+
paths = append(paths, userConfig+dropInSuffix)
260+
}
261+
262+
for _, path := range paths {
263+
entries, err := os.ReadDir(path)
264+
if err != nil {
265+
if errors.Is(err, fs.ErrNotExist) {
266+
continue
267+
}
268+
return nil, err
269+
}
270+
for _, entry := range entries {
271+
if entry.Type().IsRegular() && strings.HasSuffix(entry.Name(), suffix) {
272+
dropInMap[entry.Name()] = filepath.Join(path, entry.Name())
273+
}
274+
}
275+
}
276+
277+
sortedNames := slices.Sorted(maps.Keys(dropInMap))
278+
files := make([]string, 0, len(sortedNames))
279+
for _, file := range sortedNames {
280+
files = append(files, dropInMap[file])
281+
}
282+
return files, nil
283+
}
284+
285+
func getDropInPaths(mainPath, suffix string, uid int) []string {
286+
paths := make([]string, 0, 3)
287+
paths = append(paths, mainPath+dropInSuffix)
288+
289+
rootless := uid > 0
290+
var specialName string
291+
if rootless {
292+
specialName = "rootless"
293+
} else {
294+
specialName = "rootful"
295+
}
296+
// insert the name after the main config name but before the extension if it has one.
297+
mainPath, cut := strings.CutSuffix(mainPath, suffix)
298+
specialPath := mainPath + "." + specialName
299+
if cut {
300+
specialPath += suffix
301+
}
302+
specialPath += dropInSuffix
303+
paths = append(paths, specialPath)
304+
if rootless {
305+
paths = append(paths, filepath.Join(specialPath, strconv.Itoa(uid)))
306+
}
307+
return paths
308+
}
309+
310+
func moduleDirectories(defaultConfig, overrideConfig, userConfig string) []string {
311+
const moduleSuffix = ".modules"
312+
modules := make([]string, 0, 3)
313+
if userConfig != "" {
314+
modules = append(modules, userConfig+moduleSuffix)
315+
}
316+
if overrideConfig != "" {
317+
modules = append(modules, overrideConfig+moduleSuffix)
318+
}
319+
if defaultConfig != "" {
320+
modules = append(modules, defaultConfig+moduleSuffix)
321+
}
322+
return modules
323+
}
324+
325+
// Resolve the specified path to a module.
326+
func resolveModule(path string, dirs []string) (*os.File, error) {
327+
if filepath.IsAbs(path) {
328+
return os.Open(path)
329+
}
330+
331+
// Collect all errors to avoid suppressing important errors (e.g.,
332+
// permission errors).
333+
var multiErr error
334+
for _, d := range dirs {
335+
candidate := filepath.Join(d, path)
336+
337+
f, err := os.Open(candidate)
338+
if err == nil {
339+
return f, nil
340+
}
341+
multiErr = errors.Join(multiErr, err)
342+
}
343+
return nil, multiErr
344+
}
345+
346+
// ParseTOML parses the given config according to the rules in by [Read].
347+
// Note the given configStruct must be a pointer to a struct that describes
348+
// the toml config fields and is modified in place.
349+
// If an error is returned the struct should not be used.
350+
func ParseTOML(configStruct any, conf *File) error {
351+
for item, err := range Read(conf) {
352+
if err != nil {
353+
return err
354+
}
355+
meta, err := toml.NewDecoder(item.Reader).Decode(configStruct)
356+
if err != nil {
357+
return err
358+
}
359+
keys := meta.Undecoded()
360+
if len(keys) > 0 {
361+
logrus.Debugf("Failed to decode the keys %q from %q", keys, item.Name)
362+
}
363+
}
364+
return nil
365+
}

0 commit comments

Comments
 (0)