feat(workload): write-through workload sync + boot-time backfill
CRUD on Project / Stack / StaticSite now keeps a paired Workload row in sync. Secret setters (webhook secret, signing secret, require-signature toggle, notification secret) all re-sync after mutating the source-of-truth row so the workload row always reflects the canonical state. Delete cascades: DeleteProject/Stack/StaticSite now drop the matching workload row plus any container index entries owned by it, so global views don't show ghost rows. Boot-time BackfillWorkloads scans every project/stack/site and ensures each has a workload row. Idempotent — safe to run on every restart, recovers from a deleted/missing workload row. Behavior unchanged for existing call sites; the workloads table just starts being populated. Deployer / reconciler / consumer switchover land in the next commit.
This commit is contained in:
@@ -79,6 +79,9 @@ func (s *Store) CreateProject(p Project) (Project, error) {
|
||||
if err != nil {
|
||||
return Project{}, fmt.Errorf("insert project: %w", err)
|
||||
}
|
||||
if err := s.SyncProjectWorkload(p); err != nil {
|
||||
return Project{}, fmt.Errorf("sync project workload: %w", err)
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
@@ -173,6 +176,15 @@ func (s *Store) UpdateProject(p Project) error {
|
||||
if n == 0 {
|
||||
return fmt.Errorf("project %s: %w", p.ID, ErrNotFound)
|
||||
}
|
||||
// Re-read so the workload sync sees the canonical row (e.g. webhook
|
||||
// secrets that UpdateProject does not write but other call sites do).
|
||||
current, err := s.GetProjectByID(p.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reread project for workload sync: %w", err)
|
||||
}
|
||||
if err := s.SyncProjectWorkload(current); err != nil {
|
||||
return fmt.Errorf("sync project workload: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -190,7 +202,11 @@ func (s *Store) SetProjectWebhookSecret(id, secret string) error {
|
||||
if n == 0 {
|
||||
return fmt.Errorf("project %s: %w", id, ErrNotFound)
|
||||
}
|
||||
return nil
|
||||
current, err := s.GetProjectByID(id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reread project for workload sync: %w", err)
|
||||
}
|
||||
return s.SyncProjectWorkload(current)
|
||||
}
|
||||
|
||||
// SetProjectWebhookSigningSecret assigns the HMAC signing secret used to
|
||||
@@ -208,7 +224,11 @@ func (s *Store) SetProjectWebhookSigningSecret(id, secret string) error {
|
||||
if n == 0 {
|
||||
return fmt.Errorf("project %s: %w", id, ErrNotFound)
|
||||
}
|
||||
return nil
|
||||
current, err := s.GetProjectByID(id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reread project for workload sync: %w", err)
|
||||
}
|
||||
return s.SyncProjectWorkload(current)
|
||||
}
|
||||
|
||||
// SetProjectWebhookRequireSignature toggles whether unsigned (or
|
||||
@@ -229,7 +249,11 @@ func (s *Store) SetProjectWebhookRequireSignature(id string, require bool) error
|
||||
if n == 0 {
|
||||
return fmt.Errorf("project %s: %w", id, ErrNotFound)
|
||||
}
|
||||
return nil
|
||||
current, err := s.GetProjectByID(id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reread project for workload sync: %w", err)
|
||||
}
|
||||
return s.SyncProjectWorkload(current)
|
||||
}
|
||||
|
||||
// EnsureProjectWebhookSecret returns the current webhook secret for a project,
|
||||
@@ -265,7 +289,11 @@ func (s *Store) SetProjectNotificationSecret(id, secret string) error {
|
||||
if n == 0 {
|
||||
return fmt.Errorf("project %s: %w", id, ErrNotFound)
|
||||
}
|
||||
return nil
|
||||
current, err := s.GetProjectByID(id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reread project for workload sync: %w", err)
|
||||
}
|
||||
return s.SyncProjectWorkload(current)
|
||||
}
|
||||
|
||||
// EnsureProjectNotificationSecret returns the current outgoing-webhook signing
|
||||
@@ -287,6 +315,8 @@ func (s *Store) EnsureProjectNotificationSecret(id string) (string, error) {
|
||||
}
|
||||
|
||||
// DeleteProject removes a project by ID. Cascading deletes handle stages, instances, and deploys.
|
||||
// Workload row + container index entries are removed too so the global views
|
||||
// don't show ghost rows after a project is gone.
|
||||
func (s *Store) DeleteProject(id string) error {
|
||||
result, err := s.db.Exec(`DELETE FROM projects WHERE id = ?`, id)
|
||||
if err != nil {
|
||||
@@ -296,5 +326,13 @@ func (s *Store) DeleteProject(id string) error {
|
||||
if n == 0 {
|
||||
return fmt.Errorf("project %s: %w", id, ErrNotFound)
|
||||
}
|
||||
if w, err := s.GetWorkloadByRef(WorkloadKindProject, id); err == nil {
|
||||
if err := s.DeleteContainersByWorkload(w.ID); err != nil {
|
||||
return fmt.Errorf("delete project containers: %w", err)
|
||||
}
|
||||
if err := s.DeleteWorkload(w.ID); err != nil {
|
||||
return fmt.Errorf("delete project workload: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user