-
Notifications
You must be signed in to change notification settings - Fork 23
HYPERFLEET-1152 - feat: separate resource_conditions table for generic resources #263
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
bbed31f
6ce4fbd
ba986b8
0c037c2
5a17a41
9620c0f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| package dao | ||
|
|
||
| import ( | ||
| "context" | ||
|
|
||
| "github.com/openshift-hyperfleet/hyperfleet-api/pkg/api" | ||
| "github.com/openshift-hyperfleet/hyperfleet-api/pkg/db" | ||
| ) | ||
|
|
||
| type ResourceConditionDao interface { | ||
| // UpdateConditions must be called within a write transaction — it performs | ||
| // delete+insert which is not atomic without one. Per ADR-0008, this is called | ||
| // from the adapter status upsert path (PUT), which has a transaction from | ||
| // TransactionMiddleware. | ||
| UpdateConditions(ctx context.Context, resourceID string, conditions []api.ResourceCondition) error | ||
| } | ||
|
|
||
| var _ ResourceConditionDao = &sqlResourceConditionDao{} | ||
|
|
||
| type sqlResourceConditionDao struct { | ||
| sessionFactory db.SessionFactory | ||
| } | ||
|
|
||
| func NewResourceConditionDao(sessionFactory db.SessionFactory) ResourceConditionDao { | ||
| return &sqlResourceConditionDao{sessionFactory: sessionFactory} | ||
| } | ||
|
|
||
| func (d *sqlResourceConditionDao) UpdateConditions( | ||
| ctx context.Context, resourceID string, conditions []api.ResourceCondition, | ||
| ) error { | ||
| g2 := d.sessionFactory.New(ctx) | ||
|
|
||
| var existing []api.ResourceCondition | ||
| if err := g2.Where("resource_id = ?", resourceID).Find(&existing).Error; err != nil { | ||
| return err | ||
| } | ||
|
|
||
| prevByType := make(map[string]api.ResourceCondition, len(existing)) | ||
| for _, c := range existing { | ||
| prevByType[c.Type] = c | ||
| } | ||
|
|
||
| for i := range conditions { | ||
| conditions[i].ResourceID = resourceID | ||
| if prev, ok := prevByType[conditions[i].Type]; ok { | ||
| if prev.Status == conditions[i].Status { | ||
| conditions[i].LastTransitionTime = prev.LastTransitionTime | ||
| } | ||
| conditions[i].CreatedTime = prev.CreatedTime | ||
| } | ||
| } | ||
|
|
||
| if err := g2.Where("resource_id = ?", resourceID).Delete(&api.ResourceCondition{}).Error; err != nil { | ||
| db.MarkForRollback(ctx, err) | ||
| return err | ||
| } | ||
|
|
||
| if len(conditions) > 0 { | ||
| if err := g2.Create(&conditions).Error; err != nil { | ||
| db.MarkForRollback(ctx, err) | ||
| return err | ||
| } | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,38 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package migrations | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/go-gormigrate/gormigrate/v2" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "gorm.io/gorm" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func addResourceConditions() *gormigrate.Migration { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return &gormigrate.Migration{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ID: "202606290001", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Migrate: func(tx *gorm.DB) error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err := tx.Exec(`CREATE TABLE IF NOT EXISTS resource_conditions ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resource_id VARCHAR(255) NOT NULL, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type VARCHAR(100) NOT NULL, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status VARCHAR(10) NOT NULL, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reason TEXT, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| message TEXT, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| observed_generation INTEGER NOT NULL DEFAULT 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| created_time TIMESTAMPTZ NOT NULL DEFAULT NOW(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| last_updated_time TIMESTAMPTZ NOT NULL DEFAULT NOW(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| last_transition_time TIMESTAMPTZ NOT NULL DEFAULT NOW(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PRIMARY KEY (resource_id, type), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| FOREIGN KEY (resource_id) REFERENCES resources(id) ON DELETE CASCADE | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| );`).Error; err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+12
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Fail on schema drift instead of masking it.
Suggested change- if err := tx.Exec(`CREATE TABLE IF NOT EXISTS resource_conditions (
+ if err := tx.Exec(`CREATE TABLE resource_conditions (
resource_id VARCHAR(255) NOT NULL,
type VARCHAR(100) NOT NULL,
status VARCHAR(10) NOT NULL,
reason TEXT,
message TEXT,
observed_generation INTEGER NOT NULL DEFAULT 0,
created_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_updated_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_transition_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (resource_id, type),
FOREIGN KEY (resource_id) REFERENCES resources(id) ON DELETE CASCADE
);`).Error; err != nil {
return err
}As per path instructions, migrations must be reviewed for backward compatibility and incompatible-schema risks. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Path instructions |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err := tx.Exec( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "CREATE INDEX IF NOT EXISTS idx_resource_conditions_resource_id " + | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "ON resource_conditions (resource_id);", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ).Error; err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Mark the initial query failure for rollback.
Line 34 returns the
Find(&existing)error withoutdb.MarkForRollback(ctx, err). In this write DAO, that can commit earlier mutations in the same request transaction while condition replacement fails, leaving the resource row andresource_conditionsout of sync.As per path instructions,
pkg/dao/**: "On any write error, mark the transaction for rollback" andpkg/db/**: "Without this, the middleware will commit a partially-failed transaction."Proposed fix
var existing []api.ResourceCondition if err := g2.Where("resource_id = ?", resourceID).Find(&existing).Error; err != nil { + db.MarkForRollback(ctx, err) return err }📝 Committable suggestion
🤖 Prompt for AI Agents
Source: Path instructions