Skip to content

Commit e7d7929

Browse files
tmshortclaude
andauthored
fix(declcfg): WriteFS now preserves Others and Deprecations (#1979)
WriteFS was iterating over Packages, Channels, and Bundles, but never consulted cfg.Others or cfg.Deprecations when building the per-package DeclarativeConfig written to disk. Any custom FBC blobs (e.g. olm.lifecycle metadata used by the Operator Update Planner) and olm.deprecations entries were silently dropped. The implementation is converged with writeToEncoder by extracting a shared configsByPackage helper that groups all five schema types by package name. Both writeToEncoder and WriteFS now call this single function, so adding a new schema type to DeclarativeConfig in the future requires only one edit. Others with an empty Package field are returned as a package-less remainder and written to a root-level catalog file, matching writeToEncoder's behaviour. Fixes: OCPBUGS-85347 Signed-off-by: Todd Short <tshort@redhat.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 97a6db8 commit e7d7929

2 files changed

Lines changed: 107 additions & 49 deletions

File tree

alpha/declcfg/write.go

Lines changed: 67 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -467,52 +467,70 @@ type encoder interface {
467467
Encode(interface{}) error
468468
}
469469

470-
func writeToEncoder(cfg DeclarativeConfig, enc encoder) error {
471-
pkgNames := sets.NewString()
470+
func configsByPackage(cfg DeclarativeConfig) (sets.Set[string], map[string]DeclarativeConfig, []Meta) {
471+
pkgNames := sets.New[string]()
472+
byCfg := map[string]DeclarativeConfig{}
473+
var rootOthers []Meta
474+
475+
add := func(name string, fn func(DeclarativeConfig) DeclarativeConfig) {
476+
if name == "" {
477+
return
478+
}
479+
pkgNames.Insert(name)
480+
byCfg[name] = fn(byCfg[name])
481+
}
472482

473-
packagesByName := map[string][]Package{}
474483
for _, p := range cfg.Packages {
475-
pkgName := p.Name
476-
pkgNames.Insert(pkgName)
477-
packagesByName[pkgName] = append(packagesByName[pkgName], p)
484+
add(p.Name, func(c DeclarativeConfig) DeclarativeConfig {
485+
c.Packages = append(c.Packages, p)
486+
return c
487+
})
478488
}
479-
channelsByPackage := map[string][]Channel{}
480489
for _, c := range cfg.Channels {
481-
pkgName := c.Package
482-
pkgNames.Insert(pkgName)
483-
channelsByPackage[pkgName] = append(channelsByPackage[pkgName], c)
490+
add(c.Package, func(dc DeclarativeConfig) DeclarativeConfig {
491+
dc.Channels = append(dc.Channels, c)
492+
return dc
493+
})
484494
}
485-
bundlesByPackage := map[string][]Bundle{}
486495
for _, b := range cfg.Bundles {
487-
pkgName := b.Package
488-
pkgNames.Insert(pkgName)
489-
bundlesByPackage[pkgName] = append(bundlesByPackage[pkgName], b)
496+
add(b.Package, func(c DeclarativeConfig) DeclarativeConfig {
497+
c.Bundles = append(c.Bundles, b)
498+
return c
499+
})
490500
}
491-
othersByPackage := map[string][]Meta{}
492501
for _, o := range cfg.Others {
493-
pkgName := o.Package
494-
pkgNames.Insert(pkgName)
495-
othersByPackage[pkgName] = append(othersByPackage[pkgName], o)
502+
if o.Package == "" {
503+
rootOthers = append(rootOthers, o)
504+
continue
505+
}
506+
add(o.Package, func(c DeclarativeConfig) DeclarativeConfig {
507+
c.Others = append(c.Others, o)
508+
return c
509+
})
496510
}
497-
deprecationsByPackage := map[string][]Deprecation{}
498511
for _, d := range cfg.Deprecations {
499-
pkgName := d.Package
500-
pkgNames.Insert(pkgName)
501-
deprecationsByPackage[pkgName] = append(deprecationsByPackage[pkgName], d)
512+
add(d.Package, func(c DeclarativeConfig) DeclarativeConfig {
513+
c.Deprecations = append(c.Deprecations, d)
514+
return c
515+
})
502516
}
503517

504-
for _, pName := range pkgNames.List() {
505-
if len(pName) == 0 {
506-
continue
507-
}
508-
pkgs := packagesByName[pName]
509-
for _, p := range pkgs {
518+
return pkgNames, byCfg, rootOthers
519+
}
520+
521+
func writeToEncoder(cfg DeclarativeConfig, enc encoder) error {
522+
pkgNames, byCfg, rootOthers := configsByPackage(cfg)
523+
524+
for _, pName := range sets.List(pkgNames) {
525+
pkgCfg := byCfg[pName]
526+
527+
for _, p := range pkgCfg.Packages {
510528
if err := enc.Encode(p); err != nil {
511529
return err
512530
}
513531
}
514532

515-
channels := channelsByPackage[pName]
533+
channels := pkgCfg.Channels
516534
sort.Slice(channels, func(i, j int) bool {
517535
return channels[i].Name < channels[j].Name
518536
})
@@ -522,7 +540,7 @@ func writeToEncoder(cfg DeclarativeConfig, enc encoder) error {
522540
}
523541
}
524542

525-
bundles := bundlesByPackage[pName]
543+
bundles := pkgCfg.Bundles
526544
sort.Slice(bundles, func(i, j int) bool {
527545
return bundles[i].Name < bundles[j].Name
528546
})
@@ -532,7 +550,7 @@ func writeToEncoder(cfg DeclarativeConfig, enc encoder) error {
532550
}
533551
}
534552

535-
others := othersByPackage[pName]
553+
others := pkgCfg.Others
536554
sort.SliceStable(others, func(i, j int) bool {
537555
return others[i].Schema < others[j].Schema
538556
})
@@ -552,15 +570,14 @@ func writeToEncoder(cfg DeclarativeConfig, enc encoder) error {
552570
// Deprecation object for a package, and it would bypass validation if this
553571
// function gets called without conversion.
554572
//
555-
deprecations := deprecationsByPackage[pName]
556-
for _, d := range deprecations {
573+
for _, d := range pkgCfg.Deprecations {
557574
if err := enc.Encode(d); err != nil {
558575
return err
559576
}
560577
}
561578
}
562579

563-
for _, o := range othersByPackage[""] {
580+
for _, o := range rootOthers {
564581
if err := enc.Encode(o); err != nil {
565582
return err
566583
}
@@ -572,34 +589,35 @@ func writeToEncoder(cfg DeclarativeConfig, enc encoder) error {
572589
type WriteFunc func(config DeclarativeConfig, w io.Writer) error
573590

574591
func WriteFS(cfg DeclarativeConfig, rootDir string, writeFunc WriteFunc, fileExt string) error {
575-
channelsByPackage := map[string][]Channel{}
576-
for _, c := range cfg.Channels {
577-
channelsByPackage[c.Package] = append(channelsByPackage[c.Package], c)
578-
}
579-
bundlesByPackage := map[string][]Bundle{}
580-
for _, b := range cfg.Bundles {
581-
bundlesByPackage[b.Package] = append(bundlesByPackage[b.Package], b)
582-
}
592+
pkgNames, byCfg, rootOthers := configsByPackage(cfg)
583593

584594
if err := os.MkdirAll(rootDir, 0777); err != nil {
585595
return err
586596
}
587597

588-
for _, p := range cfg.Packages {
589-
fcfg := DeclarativeConfig{
590-
Packages: []Package{p},
591-
Channels: channelsByPackage[p.Name],
592-
Bundles: bundlesByPackage[p.Name],
598+
for _, pName := range sets.List(pkgNames) {
599+
if !filepath.IsLocal(pName) {
600+
return fmt.Errorf("invalid package name %q: must be a single local path element", pName)
593601
}
594-
pkgDir := filepath.Join(rootDir, p.Name)
602+
pkgDir := filepath.Join(rootDir, pName)
595603
if err := os.MkdirAll(pkgDir, 0777); err != nil {
596604
return err
597605
}
598606
filename := filepath.Join(pkgDir, fmt.Sprintf("catalog%s", fileExt))
599-
if err := writeFile(fcfg, filename, writeFunc); err != nil {
607+
if err := writeFile(byCfg[pName], filename, writeFunc); err != nil {
608+
return err
609+
}
610+
}
611+
612+
// Others with no package name cannot belong to any package directory;
613+
// write them to a root-level catalog file, consistent with writeToEncoder.
614+
if len(rootOthers) > 0 {
615+
filename := filepath.Join(rootDir, fmt.Sprintf("catalog%s", fileExt))
616+
if err := writeFile(DeclarativeConfig{Others: rootOthers}, filename, writeFunc); err != nil {
600617
return err
601618
}
602619
}
620+
603621
return nil
604622
}
605623

alpha/declcfg/write_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package declcfg
22

33
import (
44
"bytes"
5+
"context"
56
"encoding/json"
7+
"os"
8+
"path/filepath"
69
"testing"
710

811
"github.com/stretchr/testify/require"
@@ -621,3 +624,40 @@ class anakin-dark-anakin.v0.1.0 skipped
621624
})
622625
}
623626
}
627+
628+
func TestWriteFS(t *testing.T) {
629+
cfg := buildValidDeclarativeConfig(validDeclarativeConfigSpec{IncludeUnrecognized: true, IncludeDeprecations: true})
630+
631+
dir := t.TempDir()
632+
require.NoError(t, WriteFS(cfg, dir, WriteJSON, ".json"))
633+
634+
// Per-package files must include Others and Deprecations for that package.
635+
anakinData, err := os.ReadFile(filepath.Join(dir, "anakin", "catalog.json"))
636+
require.NoError(t, err)
637+
anakinStr := string(anakinData)
638+
require.Contains(t, anakinStr, `"schema": "custom.3"`, "anakin catalog must contain Others")
639+
require.Contains(t, anakinStr, `"schema": "olm.deprecations"`, "anakin catalog must contain Deprecations")
640+
641+
bobData, err := os.ReadFile(filepath.Join(dir, "boba-fett", "catalog.json"))
642+
require.NoError(t, err)
643+
bobStr := string(bobData)
644+
require.Contains(t, bobStr, `"schema": "custom.3"`, "boba-fett catalog must contain Others")
645+
646+
// Others with no package name must appear in a root-level catalog file.
647+
rootData, err := os.ReadFile(filepath.Join(dir, "catalog.json"))
648+
require.NoError(t, err)
649+
rootStr := string(rootData)
650+
require.Contains(t, rootStr, `"schema": "custom.1"`, "root catalog must contain package-less Others")
651+
require.Contains(t, rootStr, `"schema": "custom.2"`, "root catalog must contain package-less Others")
652+
653+
// Round-trip: loading the written files must reproduce the Others and Deprecations.
654+
// Normalize JSON whitespace in Blob fields before comparing because LoadFS calls
655+
// Meta.UnmarshalJSON, which re-encodes the raw blob through a map[string]interface{}
656+
// and therefore compacts whitespace and normalizes key order.
657+
loaded, err := LoadFS(context.Background(), os.DirFS(dir))
658+
require.NoError(t, err)
659+
removeJSONWhitespace(&cfg)
660+
removeJSONWhitespace(loaded)
661+
require.ElementsMatch(t, cfg.Others, loaded.Others)
662+
require.ElementsMatch(t, cfg.Deprecations, loaded.Deprecations)
663+
}

0 commit comments

Comments
 (0)