Skip to content

Commit eec8100

Browse files
committed
add WalkMetaFS function
This function is like WalkFS except that callers provide a function that handles a Meta object at a time, rather than an entire file's worth of FBC objects. For callers that can operate on a single meta at a time, this function will provide significant reductions in memory usage. Signed-off-by: Joe Lanford <[email protected]>
1 parent 0aebc3c commit eec8100

File tree

3 files changed

+144
-31
lines changed

3 files changed

+144
-31
lines changed

alpha/declcfg/declcfg.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ type RelatedImage struct {
8484
type Meta struct {
8585
Schema string
8686
Package string
87+
Name string
8788

8889
Blob json.RawMessage
8990
}
@@ -96,6 +97,7 @@ func (m *Meta) UnmarshalJSON(blob []byte) error {
9697
type tmp struct {
9798
Schema string `json:"schema"`
9899
Package string `json:"package,omitempty"`
100+
Name string `json:"name,omitempty"`
99101
Properties []property.Property `json:"properties,omitempty"`
100102
}
101103
var t tmp
@@ -104,6 +106,7 @@ func (m *Meta) UnmarshalJSON(blob []byte) error {
104106
}
105107
m.Schema = t.Schema
106108
m.Package = t.Package
109+
m.Name = t.Name
107110
m.Blob = blob
108111
return nil
109112
}

alpha/declcfg/load.go

Lines changed: 74 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,48 @@ const (
2222
indexIgnoreFilename = ".indexignore"
2323
)
2424

25+
type WalkMetasFunc func(path string, meta *Meta, err error) error
26+
27+
func WalkMetasFS(root fs.FS, walkFn WalkMetasFunc) error {
28+
return walkFiles(root, func(root fs.FS, path string, err error) error {
29+
if err != nil {
30+
return walkFn(path, nil, err)
31+
}
32+
33+
f, err := root.Open(path)
34+
if err != nil {
35+
return walkFn(path, nil, err)
36+
}
37+
defer f.Close()
38+
39+
return walkMetasReader(f, func(meta *Meta) error {
40+
return walkFn(path, meta, nil)
41+
})
42+
})
43+
}
44+
2545
type WalkFunc func(path string, cfg *DeclarativeConfig, err error) error
2646

2747
// WalkFS walks root using a gitignore-style filename matcher to skip files
2848
// that match patterns found in .indexignore files found throughout the filesystem.
2949
// It calls walkFn for each declarative config file it finds. If WalkFS encounters
3050
// an error loading or parsing any file, the error will be immediately returned.
3151
func WalkFS(root fs.FS, walkFn WalkFunc) error {
52+
return walkFiles(root, func(root fs.FS, path string, err error) error {
53+
if err != nil {
54+
return walkFn(path, nil, err)
55+
}
56+
57+
cfg, err := LoadFile(root, path)
58+
if err != nil {
59+
return walkFn(path, cfg, err)
60+
}
61+
62+
return walkFn(path, cfg, nil)
63+
})
64+
}
65+
66+
func walkFiles(root fs.FS, fn func(root fs.FS, path string, err error) error) error {
3267
if root == nil {
3368
return fmt.Errorf("no declarative config filesystem provided")
3469
}
@@ -40,20 +75,15 @@ func WalkFS(root fs.FS, walkFn WalkFunc) error {
4075

4176
return fs.WalkDir(root, ".", func(path string, info fs.DirEntry, err error) error {
4277
if err != nil {
43-
return walkFn(path, nil, err)
78+
return fn(root, path, err)
4479
}
4580
// avoid validating a directory, an .indexignore file, or any file that matches
4681
// an ignore pattern outlined in a .indexignore file.
4782
if info.IsDir() || info.Name() == indexIgnoreFilename || matcher.Match(path, false) {
4883
return nil
4984
}
5085

51-
cfg, err := LoadFile(root, path)
52-
if err != nil {
53-
return walkFn(path, cfg, err)
54-
}
55-
56-
return walkFn(path, cfg, err)
86+
return fn(root, path, nil)
5787
})
5888
}
5989

@@ -123,46 +153,35 @@ func extractCSV(objs []string) string {
123153
// Path references will not be de-referenced so callers are responsible for de-referencing if necessary.
124154
func LoadReader(r io.Reader) (*DeclarativeConfig, error) {
125155
cfg := &DeclarativeConfig{}
126-
dec := yaml.NewYAMLOrJSONDecoder(r, 4096)
127-
for {
128-
doc := json.RawMessage{}
129-
if err := dec.Decode(&doc); err != nil {
130-
if errors.Is(err, io.EOF) {
131-
break
132-
}
133-
return nil, err
134-
}
135-
doc = []byte(strings.NewReplacer(`\u003c`, "<", `\u003e`, ">", `\u0026`, "&").Replace(string(doc)))
136-
137-
var in Meta
138-
if err := json.Unmarshal(doc, &in); err != nil {
139-
return nil, fmt.Errorf("unmarshal error: %s", resolveUnmarshalErr(doc, err))
140-
}
141156

157+
if err := walkMetasReader(r, func(in *Meta) error {
142158
switch in.Schema {
143159
case SchemaPackage:
144160
var p Package
145-
if err := json.Unmarshal(doc, &p); err != nil {
146-
return nil, fmt.Errorf("parse package: %v", err)
161+
if err := json.Unmarshal(in.Blob, &p); err != nil {
162+
return fmt.Errorf("parse package: %v", err)
147163
}
148164
cfg.Packages = append(cfg.Packages, p)
149165
case SchemaChannel:
150166
var c Channel
151-
if err := json.Unmarshal(doc, &c); err != nil {
152-
return nil, fmt.Errorf("parse channel: %v", err)
167+
if err := json.Unmarshal(in.Blob, &c); err != nil {
168+
return fmt.Errorf("parse channel: %v", err)
153169
}
154170
cfg.Channels = append(cfg.Channels, c)
155171
case SchemaBundle:
156172
var b Bundle
157-
if err := json.Unmarshal(doc, &b); err != nil {
158-
return nil, fmt.Errorf("parse bundle: %v", err)
173+
if err := json.Unmarshal(in.Blob, &b); err != nil {
174+
return fmt.Errorf("parse bundle: %v", err)
159175
}
160176
cfg.Bundles = append(cfg.Bundles, b)
161177
case "":
162-
return nil, fmt.Errorf("object '%s' is missing root schema field", string(doc))
178+
return fmt.Errorf("object '%s' is missing root schema field", string(in.Blob))
163179
default:
164-
cfg.Others = append(cfg.Others, in)
180+
cfg.Others = append(cfg.Others, *in)
165181
}
182+
return nil
183+
}); err != nil {
184+
return nil, err
166185
}
167186
return cfg, nil
168187
}
@@ -188,6 +207,30 @@ func LoadFile(root fs.FS, path string) (*DeclarativeConfig, error) {
188207
return cfg, nil
189208
}
190209

210+
func walkMetasReader(r io.Reader, fn func(*Meta) error) error {
211+
dec := yaml.NewYAMLOrJSONDecoder(r, 4096)
212+
for {
213+
doc := json.RawMessage{}
214+
if err := dec.Decode(&doc); err != nil {
215+
if errors.Is(err, io.EOF) {
216+
break
217+
}
218+
return err
219+
}
220+
doc = []byte(strings.NewReplacer(`\u003c`, "<", `\u003e`, ">", `\u0026`, "&").Replace(string(doc)))
221+
222+
var in Meta
223+
if err := json.Unmarshal(doc, &in); err != nil {
224+
return fmt.Errorf("unmarshal error: %s", resolveUnmarshalErr(doc, err))
225+
}
226+
227+
if err := fn(&in); err != nil {
228+
return err
229+
}
230+
}
231+
return nil
232+
}
233+
191234
func resolveUnmarshalErr(data []byte, err error) string {
192235
var te *json.UnmarshalTypeError
193236
if errors.As(err, &te) {
@@ -213,10 +256,10 @@ func formatUnmarshallErrorString(data []byte, errmsg string, offset int64) strin
213256
var pOffset, origOffset int64
214257
origOffset = 0
215258
for origOffset = 0; origOffset < offset; {
216-
pOffset++
217259
if pString[pOffset] != '\n' && pString[pOffset] != ' ' {
218260
origOffset++
219261
}
262+
pOffset++
220263
}
221264
_, _ = sb.WriteString(pString[:pOffset])
222265
_, _ = sb.WriteString(" <== ")

alpha/declcfg/load_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,73 @@ func TestLoadReader(t *testing.T) {
9292
}
9393
}
9494

95+
func TestWalkMetasFS(t *testing.T) {
96+
type spec struct {
97+
name string
98+
fsys fs.FS
99+
assertion require.ErrorAssertionFunc
100+
expectNumPackages int
101+
expectNumChannels int
102+
expectNumBundles int
103+
expectNumOthers int
104+
}
105+
specs := []spec{
106+
{
107+
name: "Error/NilFS",
108+
fsys: nil,
109+
assertion: require.Error,
110+
},
111+
{
112+
name: "Error/NonExistentDir",
113+
fsys: os.DirFS("non/existent/dir/"),
114+
assertion: require.Error,
115+
},
116+
{
117+
name: "Error/Invalid",
118+
fsys: invalidFS,
119+
assertion: require.Error,
120+
},
121+
{
122+
name: "Success/ValidDir",
123+
fsys: validFS,
124+
assertion: require.NoError,
125+
expectNumPackages: 3,
126+
expectNumChannels: 0,
127+
expectNumBundles: 12,
128+
expectNumOthers: 1,
129+
},
130+
}
131+
132+
for _, s := range specs {
133+
t.Run(s.name, func(t *testing.T) {
134+
numPackages, numChannels, numBundles, numOthers := 0, 0, 0, 0
135+
err := WalkMetasFS(s.fsys, func(path string, meta *Meta, err error) error {
136+
if err != nil {
137+
return err
138+
}
139+
switch meta.Schema {
140+
case SchemaPackage:
141+
numPackages++
142+
case SchemaChannel:
143+
numChannels++
144+
case SchemaBundle:
145+
numBundles++
146+
default:
147+
numOthers++
148+
}
149+
return nil
150+
})
151+
s.assertion(t, err)
152+
if err == nil {
153+
assert.Equal(t, s.expectNumPackages, numPackages, "unexpected package count")
154+
assert.Equal(t, s.expectNumChannels, numChannels, "unexpected channel count")
155+
assert.Equal(t, s.expectNumBundles, numBundles, "unexpected bundle count")
156+
assert.Equal(t, s.expectNumOthers, numOthers, "unexpected others count")
157+
}
158+
})
159+
}
160+
}
161+
95162
func TestLoadFS(t *testing.T) {
96163
type spec struct {
97164
name string

0 commit comments

Comments
 (0)