From 8fbd0b9b83ffa58de7cc766d14b6888c88024564 Mon Sep 17 00:00:00 2001 From: Stephen Benjamin Date: Mon, 23 Feb 2026 16:46:52 -0500 Subject: [PATCH] Fix feature gate filename parsing for major version segmentation openshift/api#2637 introduced major version segmentation for feature gates, changing filenames from featureGate-{topology}-{featureSet}.yaml to featureGate-{majorStart}-{majorEnd}-{topology}-{featureSet}.yaml. The parser was extracting the version range components as topology/featureSet, causing all 4.22 files to collide on the same composite key and fail with "ON CONFLICT DO UPDATE command cannot affect row a second time". Fix by taking the last two dash-separated segments as topology and featureSet, which works for both old and new filename formats. Note: the new files include annotations that could be used instead of filename parsing: release.openshift.io/feature-set for the feature set, include.release.openshift.io/* for the topology (e.g. ibm-cloud-managed maps to Hypershift, self-managed-high-availability maps to SelfManagedHA). The old format (4.21 and earlier) lacks the feature-set annotation but does have the topology include annotation. A future improvement could switch to annotation-based parsing for better reliability, but for now this unbreaks the loader. Co-Authored-By: Claude Opus 4.6 --- .../featuregateloader/featuregateloader.go | 6 +- .../featuregateloader_test.go | 79 +++++++++++++++++++ 2 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 pkg/dataloader/featuregateloader/featuregateloader_test.go diff --git a/pkg/dataloader/featuregateloader/featuregateloader.go b/pkg/dataloader/featuregateloader/featuregateloader.go index 0f0232b882..ab2ef4cc8f 100644 --- a/pkg/dataloader/featuregateloader/featuregateloader.go +++ b/pkg/dataloader/featuregateloader/featuregateloader.go @@ -175,13 +175,15 @@ func processFeatureGateFile(path, release, filename string) ([]models.FeatureGat return convertAPIToDB(fg, release, topology, featureSet, path), nil } -// parseFeatureGateFilename extracts topology and feature set from the filename +// parseFeatureGateFilename extracts topology and feature set from the filename. +// Handles both old format (featureGate-{topology}-{featureSet}.yaml) and +// new versioned format (featureGate-{majorStart}-{majorEnd}-{topology}-{featureSet}.yaml). func parseFeatureGateFilename(filename string) (string, string, bool) { parts := strings.Split(strings.TrimSuffix(filename, ".yaml"), "-") if len(parts) < 3 { return "", "", false } - return parts[1], parts[2], true + return parts[len(parts)-2], parts[len(parts)-1], true } // convertAPIToDB converts the parsed feature gate data into db models diff --git a/pkg/dataloader/featuregateloader/featuregateloader_test.go b/pkg/dataloader/featuregateloader/featuregateloader_test.go new file mode 100644 index 0000000000..685a8723c6 --- /dev/null +++ b/pkg/dataloader/featuregateloader/featuregateloader_test.go @@ -0,0 +1,79 @@ +package featuregateloader + +import ( + "testing" +) + +func TestParseFeatureGateFilename(t *testing.T) { + tests := []struct { + name string + filename string + wantTopo string + wantFeatSet string + wantValid bool + }{ + { + name: "old format Hypershift Default", + filename: "featureGate-Hypershift-Default.yaml", + wantTopo: "Hypershift", + wantFeatSet: "Default", + wantValid: true, + }, + { + name: "old format SelfManagedHA TechPreviewNoUpgrade", + filename: "featureGate-SelfManagedHA-TechPreviewNoUpgrade.yaml", + wantTopo: "SelfManagedHA", + wantFeatSet: "TechPreviewNoUpgrade", + wantValid: true, + }, + { + name: "versioned format Hypershift Default", + filename: "featureGate-4-10-Hypershift-Default.yaml", + wantTopo: "Hypershift", + wantFeatSet: "Default", + wantValid: true, + }, + { + name: "versioned format SelfManagedHA DevPreviewNoUpgrade", + filename: "featureGate-4-10-SelfManagedHA-DevPreviewNoUpgrade.yaml", + wantTopo: "SelfManagedHA", + wantFeatSet: "DevPreviewNoUpgrade", + wantValid: true, + }, + { + name: "versioned format Hypershift OKD", + filename: "featureGate-4-10-Hypershift-OKD.yaml", + wantTopo: "Hypershift", + wantFeatSet: "OKD", + wantValid: true, + }, + { + name: "too few parts", + filename: "featureGate-Default.yaml", + wantValid: false, + }, + { + name: "just prefix", + filename: "featureGate.yaml", + wantValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotTopo, gotFeatSet, gotValid := parseFeatureGateFilename(tt.filename) + if gotValid != tt.wantValid { + t.Fatalf("valid = %v, want %v", gotValid, tt.wantValid) + } + if !gotValid { + return + } + if gotTopo != tt.wantTopo { + t.Errorf("topology = %q, want %q", gotTopo, tt.wantTopo) + } + if gotFeatSet != tt.wantFeatSet { + t.Errorf("featureSet = %q, want %q", gotFeatSet, tt.wantFeatSet) + } + }) + } +}