Skip to content

Commit c3dca6d

Browse files
mstaebleclaude
andcommitted
Move release metadata from BigQuery to PostgreSQL
Create a release_definitions table to store release metadata (GA dates, development start dates, previous release, capabilities, product, status) that was previously only available in BigQuery. This eliminates the BQ dependency for the /api/releases endpoint and removes the hardcoded releaseMetadata map from the PostgreSQL data provider, which required manual updates for each new release. Key changes: - Add ReleaseDefinition model with capability constants and HasCapability method - Add release-definitions loader (--loader release-definitions) that fetches from BQ and syncs to PG via upsert - getReleases() in the server prefers PG, falls back to BQ - PG data provider QueryReleases() reads from release_definitions instead of deriving from prow_jobs + hardcoded map - Seed data populates release_definitions for local development - Fix stale "from big query" error messages in server.go Ref: TRT-2734 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9eff376 commit c3dca6d

9 files changed

Lines changed: 297 additions & 80 deletions

File tree

cmd/sippy/load.go

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
"github.com/openshift/sippy/pkg/dataloader/prowloader"
3838
"github.com/openshift/sippy/pkg/dataloader/prowloader/gcs"
3939
"github.com/openshift/sippy/pkg/dataloader/prowloader/github"
40+
releasedefloader "github.com/openshift/sippy/pkg/dataloader/releasedefloader"
4041
"github.com/openshift/sippy/pkg/dataloader/releaseloader"
4142
"github.com/openshift/sippy/pkg/dataloader/testownershiploader"
4243
"github.com/openshift/sippy/pkg/db"
@@ -100,7 +101,7 @@ func (f *LoadFlags) BindFlags(fs *pflag.FlagSet) {
100101
f.JiraFlags.BindFlags(fs)
101102

102103
fs.BoolVar(&f.InitDatabase, "init-database", false, "Migrate the DB before loading")
103-
fs.StringArrayVar(&f.Loaders, "loader", []string{"prow", "releases", "jira", "github", "bugs", "test-mapping", "feature-gates"}, "Which data sources to use for data loading")
104+
fs.StringArrayVar(&f.Loaders, "loader", []string{"release-definitions", "prow", "releases", "jira", "github", "bugs", "test-mapping", "feature-gates"}, "Which data sources to use for data loading")
104105
fs.StringArrayVar(&f.Releases, "release", f.Releases, "Which releases to load (one per arg instance)")
105106
fs.StringArrayVar(&f.Architectures, "arch", f.Architectures, "Which architectures to load (one per arg instance)")
106107
fs.StringVar(&f.JobVariantsInputFile, "job-variants-input-file", "expected-job-variants.json", "JSON input file for the job-variants loader")
@@ -152,8 +153,6 @@ func NewLoadCommand() *cobra.Command {
152153
cacheClient = nil // error hygiene, since we pass this down to quite a few functions
153154
}
154155

155-
releaseConfigs := []sippyv1.Release{}
156-
157156
// initializing a bigquery client different from the normal one
158157
opCtx, ctx := bqcachedclient.OpCtxForCronEnv(ctx, "load")
159158
bqc, bigqueryErr := bqcachedclient.New(
@@ -164,10 +163,14 @@ func NewLoadCommand() *cobra.Command {
164163
if f.CacheFlags.EnablePersistentCaching {
165164
bqc = f.CacheFlags.DecorateBiqQueryClientWithPersistentCache(bqc)
166165
}
167-
releaseConfigs, err = api.GetReleasesFromBigQuery(context.Background(), bqc)
168-
if err != nil {
169-
return errors.Wrapf(err, "error querying releases from bq")
170-
}
166+
}
167+
168+
// Read release definitions from PG for downstream loader construction.
169+
// On the first run the table may be empty; the release-definitions
170+
// loader will populate it for subsequent runs.
171+
releaseConfigs := []sippyv1.Release{}
172+
if dbErr == nil {
173+
releaseConfigs, _ = api.GetReleasesFromDB(context.Background(), dbc)
171174
}
172175

173176
// Ensure partitions exist for all releases (only when InitDatabase is true)
@@ -201,6 +204,17 @@ func NewLoadCommand() *cobra.Command {
201204

202205
var regressionCacheAdded bool
203206
for _, l := range f.Loaders {
207+
if l == "release-definitions" {
208+
if bigqueryErr != nil {
209+
return errors.Wrap(bigqueryErr, "CRITICAL error getting BigQuery client which prevents release-definitions loading")
210+
}
211+
if dbErr != nil {
212+
return errors.Wrap(dbErr, "CRITICAL error getting postgres client which prevents release-definitions loading")
213+
}
214+
rdl := releasedefloader.NewReleaseDefinitionLoader(ctx, dbc, bqc)
215+
loaders = append(loaders, rdl)
216+
}
217+
204218
// TODO: remove "component-readiness-cache" and "regression-tracker" once the cronjob
205219
// manifests are updated to use "regression-cache".
206220
if l == "component-readiness-cache" || l == "regression-tracker" || l == "regression-cache" {

cmd/sippy/seed_data.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,11 @@ func seedSyntheticData(dbc *db.DB) error {
411411
return nil
412412
}
413413

414+
if err := seedReleaseDefinitions(dbc); err != nil {
415+
return errors.WithMessage(err, "failed to seed release definitions")
416+
}
417+
log.Info("Seeded release definitions")
418+
414419
if err := createTestSuite(dbc, "synthetic"); err != nil {
415420
return errors.WithMessage(err, "failed to create test suite")
416421
}
@@ -447,6 +452,53 @@ func seedSyntheticData(dbc *db.DB) error {
447452
return nil
448453
}
449454

455+
func seedReleaseDefinitions(dbc *db.DB) error {
456+
now := time.Now().UTC()
457+
allCaps := pq.StringArray{models.CapComponentReadiness, models.CapFeatureGates, models.CapMetrics, models.CapPayloadTags, models.CapSippyClassic}
458+
459+
type relMeta struct {
460+
previous string
461+
gaDays int // negative = days before now; 0 = no GA (in development)
462+
}
463+
meta := map[string]relMeta{
464+
"4.19": {previous: "4.18", gaDays: -289},
465+
"4.20": {previous: "4.19", gaDays: -163},
466+
"4.21": {previous: "4.20", gaDays: -58},
467+
"4.22": {previous: "4.21"},
468+
}
469+
470+
for _, release := range syntheticReleases {
471+
m := meta[release]
472+
parts := strings.Split(release, ".")
473+
major, minor := 0, 0
474+
if len(parts) >= 2 {
475+
fmt.Sscanf(parts[0], "%d", &major)
476+
fmt.Sscanf(parts[1], "%d", &minor)
477+
}
478+
479+
develStart := now.AddDate(0, 0, m.gaDays-180)
480+
def := models.ReleaseDefinition{
481+
Release: release,
482+
Major: major,
483+
Minor: minor,
484+
PreviousRelease: m.previous,
485+
DevelopmentStartDate: &develStart,
486+
Product: "OCP",
487+
Status: "Full Support",
488+
Capabilities: allCaps,
489+
}
490+
if m.gaDays != 0 {
491+
ga := now.AddDate(0, 0, m.gaDays)
492+
def.GADate = &ga
493+
}
494+
495+
if err := dbc.DB.Where("release = ?", release).FirstOrCreate(&def).Error; err != nil {
496+
return fmt.Errorf("failed to create release definition %s: %w", release, err)
497+
}
498+
}
499+
return nil
500+
}
501+
450502
func seedProwJobs(dbc *db.DB) error {
451503
for _, release := range syntheticReleases {
452504
for _, job := range syntheticJobs {

pkg/api/componentreadiness/dataprovider/postgres/provider.go

Lines changed: 2 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/lib/pq"
1313

14+
"github.com/openshift/sippy/pkg/api"
1415
"github.com/openshift/sippy/pkg/api/componentreadiness/dataprovider"
1516
"github.com/openshift/sippy/pkg/api/componentreadiness/utils"
1617
"github.com/openshift/sippy/pkg/apis/api/componentreport/crstatus"
@@ -132,60 +133,8 @@ func (p *PostgresProvider) QueryJobVariants(ctx context.Context) (crtest.JobVari
132133
return variants, nil
133134
}
134135

135-
// releaseMetadata holds hardcoded release info for known releases.
136-
// This avoids needing a releases table — we derive release names from prow_jobs
137-
// and fill in metadata from this map.
138-
var releaseMetadata = map[string]struct {
139-
previousRelease string
140-
gaOffsetDays int // 0 = no GA date (in development)
141-
product string // empty = defaults to "OCP"
142-
}{
143-
"4.17": {previousRelease: "4.16", gaOffsetDays: -540},
144-
"4.18": {previousRelease: "4.17", gaOffsetDays: -395},
145-
"4.19": {previousRelease: "4.18", gaOffsetDays: -289},
146-
"4.20": {previousRelease: "4.19", gaOffsetDays: -163},
147-
"4.21": {previousRelease: "4.20", gaOffsetDays: -58},
148-
"4.22": {previousRelease: "4.21"},
149-
"5.0": {previousRelease: "4.22"},
150-
}
151-
152136
func (p *PostgresProvider) QueryReleases(ctx context.Context) ([]v1.Release, error) {
153-
var releaseNames []string
154-
err := p.dbc.DB.WithContext(ctx).Raw(`SELECT DISTINCT release FROM prow_jobs WHERE deleted_at IS NULL ORDER BY release DESC`).
155-
Pluck("release", &releaseNames).Error
156-
if err != nil {
157-
return nil, fmt.Errorf("querying releases: %w", err)
158-
}
159-
160-
caps := map[v1.ReleaseCapability]bool{
161-
v1.ComponentReadinessCap: true,
162-
v1.FeatureGatesCap: true,
163-
v1.MetricsCap: true,
164-
v1.PayloadTagsCap: true,
165-
v1.SippyClassicCap: true,
166-
}
167-
168-
now := time.Now().UTC()
169-
var releases []v1.Release
170-
for _, name := range releaseNames {
171-
rel := v1.Release{
172-
Release: name,
173-
Capabilities: caps,
174-
Product: "OCP",
175-
}
176-
if meta, ok := releaseMetadata[name]; ok {
177-
rel.PreviousRelease = meta.previousRelease
178-
if meta.gaOffsetDays != 0 {
179-
ga := now.AddDate(0, 0, meta.gaOffsetDays)
180-
rel.GADate = &ga
181-
}
182-
if meta.product != "" {
183-
rel.Product = meta.product
184-
}
185-
}
186-
releases = append(releases, rel)
187-
}
188-
return releases, nil
137+
return api.GetReleasesFromDB(ctx, p.dbc)
189138
}
190139

191140
func (p *PostgresProvider) QueryReleaseDates(ctx context.Context, _ reqopts.RequestOptions) ([]crtest.ReleaseTimeRange, []error) {

pkg/api/releases.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,66 @@ func transformRelease(r sippyv1.ReleaseRow) sippyv1.Release {
498498
return release
499499
}
500500

501+
// GetReleaseRowsFromBigQuery fetches raw release rows from BigQuery's Releases table.
502+
func GetReleaseRowsFromBigQuery(ctx context.Context, client *bqcachedclient.Client) ([]sippyv1.ReleaseRow, error) {
503+
var rows []sippyv1.ReleaseRow
504+
505+
queryString := fmt.Sprintf("SELECT * FROM `%s` ORDER BY DevelStartDate DESC", client.ReleasesTable)
506+
507+
q := client.Query(ctx, bqlabel.ReleaseAllReleases, queryString)
508+
it, err := q.Read(ctx)
509+
if err != nil {
510+
log.WithError(err).Error("error querying releases data from bigquery")
511+
return rows, err
512+
}
513+
514+
for {
515+
r := sippyv1.ReleaseRow{}
516+
err := it.Next(&r)
517+
if err == iterator.Done {
518+
break
519+
}
520+
if err != nil {
521+
log.WithError(err).Error("error parsing release row from bigquery")
522+
return rows, err
523+
}
524+
rows = append(rows, r)
525+
}
526+
return rows, nil
527+
}
528+
529+
// GetReleasesFromDB queries release metadata from the release_definitions table
530+
// and converts to []sippyv1.Release for use by existing callers.
531+
func GetReleasesFromDB(ctx context.Context, dbc *db.DB) ([]sippyv1.Release, error) {
532+
var defs []models.ReleaseDefinition
533+
err := dbc.DB.WithContext(ctx).Order("development_start_date DESC").Find(&defs).Error
534+
if err != nil {
535+
return nil, fmt.Errorf("querying release definitions: %w", err)
536+
}
537+
releases := make([]sippyv1.Release, 0, len(defs))
538+
for _, def := range defs {
539+
releases = append(releases, DefinitionToRelease(def))
540+
}
541+
return releases, nil
542+
}
543+
544+
// DefinitionToRelease converts a models.ReleaseDefinition to a sippyv1.Release.
545+
func DefinitionToRelease(def models.ReleaseDefinition) sippyv1.Release {
546+
caps := make(map[sippyv1.ReleaseCapability]bool, len(def.Capabilities))
547+
for _, cap := range def.Capabilities {
548+
caps[sippyv1.ReleaseCapability(cap)] = true
549+
}
550+
return sippyv1.Release{
551+
Release: def.Release,
552+
Status: def.Status,
553+
GADate: def.GADate,
554+
DevelopmentStartDate: def.DevelopmentStartDate,
555+
PreviousRelease: def.PreviousRelease,
556+
Capabilities: caps,
557+
Product: def.Product,
558+
}
559+
}
560+
501561
// BuildReleasesResponse creates the API response structure for releases
502562
func BuildReleasesResponse(releases []sippyv1.Release, lastUpdated time.Time) apitype.Releases {
503563
gaDateMap := make(map[string]time.Time)

pkg/dataloader/loaderwithmetrics/loaderwithmetrics.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func New(wrappedLoaders []dataloader.DataLoader) *LoaderWithMetrics {
4646
return loader
4747
}
4848

49-
var loaderOrder = []string{"prow", "releases", "jira", "github", "bugs", "test-mapping", "feature-gates", "regression-cache"}
49+
var loaderOrder = []string{"release-definitions", "prow", "releases", "jira", "github", "bugs", "test-mapping", "feature-gates", "regression-cache"}
5050

5151
// sortLoaders guarantees that the loaders run in a predictable and proper order
5252
func (l *LoaderWithMetrics) sortLoaders() {
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package releasedefloader
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"sort"
7+
"time"
8+
9+
"github.com/lib/pq"
10+
log "github.com/sirupsen/logrus"
11+
"gorm.io/gorm/clause"
12+
13+
"github.com/openshift/sippy/pkg/api"
14+
v1 "github.com/openshift/sippy/pkg/apis/sippy/v1"
15+
bqcachedclient "github.com/openshift/sippy/pkg/bigquery"
16+
"github.com/openshift/sippy/pkg/db"
17+
"github.com/openshift/sippy/pkg/db/models"
18+
)
19+
20+
// ReleaseDefinitionLoader fetches release metadata from BigQuery and syncs it to PostgreSQL.
21+
type ReleaseDefinitionLoader struct {
22+
ctx context.Context
23+
dbc *db.DB
24+
bqClient *bqcachedclient.Client
25+
errs []error
26+
}
27+
28+
func NewReleaseDefinitionLoader(ctx context.Context, dbc *db.DB, bqClient *bqcachedclient.Client) *ReleaseDefinitionLoader {
29+
return &ReleaseDefinitionLoader{
30+
ctx: ctx,
31+
dbc: dbc,
32+
bqClient: bqClient,
33+
}
34+
}
35+
36+
func (l *ReleaseDefinitionLoader) Name() string {
37+
return "release-definitions"
38+
}
39+
40+
func (l *ReleaseDefinitionLoader) Load() {
41+
releaseRows, err := api.GetReleaseRowsFromBigQuery(l.ctx, l.bqClient)
42+
if err != nil {
43+
l.errs = append(l.errs, fmt.Errorf("fetching releases from bigquery: %w", err))
44+
return
45+
}
46+
defs := make([]models.ReleaseDefinition, 0, len(releaseRows))
47+
for _, row := range releaseRows {
48+
defs = append(defs, ReleaseRowToDefinition(row))
49+
}
50+
if err := syncReleaseDefinitions(l.dbc, defs); err != nil {
51+
l.errs = append(l.errs, fmt.Errorf("syncing release definitions: %w", err))
52+
}
53+
}
54+
55+
func (l *ReleaseDefinitionLoader) Errors() []error {
56+
return l.errs
57+
}
58+
59+
func syncReleaseDefinitions(dbc *db.DB, defs []models.ReleaseDefinition) error {
60+
for _, def := range defs {
61+
err := dbc.DB.Clauses(clause.OnConflict{
62+
Columns: []clause.Column{{Name: "release"}},
63+
DoUpdates: clause.AssignmentColumns([]string{"major", "minor", "patch", "previous_release", "ga_date", "development_start_date", "product", "status", "capabilities", "updated_at"}),
64+
}).Create(&def).Error
65+
if err != nil {
66+
return fmt.Errorf("upserting release definition %s: %w", def.Release, err)
67+
}
68+
}
69+
log.WithField("count", len(defs)).Info("synced release definitions to postgres")
70+
return nil
71+
}
72+
73+
// ReleaseRowToDefinition converts a BigQuery ReleaseRow directly to a ReleaseDefinition DB model.
74+
func ReleaseRowToDefinition(r v1.ReleaseRow) models.ReleaseDefinition {
75+
caps := make(pq.StringArray, 0, len(r.Capabilities))
76+
for _, cap := range r.Capabilities {
77+
caps = append(caps, string(cap))
78+
}
79+
sort.Strings(caps)
80+
81+
def := models.ReleaseDefinition{
82+
Release: r.Release,
83+
Major: r.Major,
84+
Minor: r.Minor,
85+
PreviousRelease: r.PreviousRelease.StringVal,
86+
Product: r.Product.StringVal,
87+
Status: r.ReleaseStatus.StringVal,
88+
Capabilities: caps,
89+
}
90+
if r.Patch.Valid {
91+
p := int(r.Patch.Int64)
92+
def.Patch = &p
93+
}
94+
if r.GADate.Valid {
95+
ga := r.GADate.Date.In(time.UTC)
96+
def.GADate = &ga
97+
}
98+
if r.DevelStartDate.IsValid() {
99+
ds := r.DevelStartDate.In(time.UTC)
100+
def.DevelopmentStartDate = &ds
101+
}
102+
return def
103+
}

pkg/db/db.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ func (d *DB) UpdateSchema(reportEnd *time.Time) error {
137137

138138
// List of all models to migrate
139139
modelsToMigrate := []any{
140+
&models.ReleaseDefinition{},
140141
&models.ReleaseTag{},
141142
&models.ReleasePullRequest{},
142143
&models.ReleaseRepository{},

0 commit comments

Comments
 (0)