181 lines
4.9 KiB
Go
181 lines
4.9 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"moku-backend/internal/jobs"
|
|
"moku-backend/internal/posixproj"
|
|
)
|
|
|
|
func (service *Service) enqueueBootstrapStructureMaterialization(
|
|
ctx context.Context,
|
|
installation *InstallationRecord,
|
|
) error {
|
|
if installation == nil {
|
|
return nil
|
|
}
|
|
|
|
jobStore := jobs.NewStore(service.db)
|
|
if _, err := jobStore.Enqueue(ctx, jobs.EnqueueInput{
|
|
Kind: jobs.KindBootstrapStructureMaterialize,
|
|
Payload: jobs.BootstrapStructureMaterializePayload{
|
|
InstallationID: installation.ID,
|
|
},
|
|
}); err != nil {
|
|
failure := fmt.Sprintf("enqueue bootstrap materialization job: %v", err)
|
|
if markErr := service.markBootstrapMaterializationFailed(ctx, installation.ID, failure); markErr != nil {
|
|
return errors.Join(err, markErr)
|
|
}
|
|
|
|
// The relational bootstrap write already committed successfully, so keep the
|
|
// response successful and surface the enqueue problem via materialization state.
|
|
installation.MaterializationStatus = materializationFailed
|
|
installation.MaterializationError = &failure
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (service *Service) ProcessBootstrapStructureMaterialization(ctx context.Context, installationID string) error {
|
|
trimmedInstallationID := strings.TrimSpace(installationID)
|
|
if trimmedInstallationID == "" {
|
|
return ErrInstallationNotConfigured
|
|
}
|
|
|
|
installation, err := service.GetInstallation(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if installation == nil || installation.ID != trimmedInstallationID {
|
|
return ErrInstallationNotConfigured
|
|
}
|
|
|
|
if err := service.markBootstrapMaterializationRunning(ctx, trimmedInstallationID); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := service.materializeBootstrapStructure(ctx, *installation); err != nil {
|
|
failure := strings.TrimSpace(err.Error())
|
|
if failure == "" {
|
|
failure = "bootstrap materialization failed"
|
|
}
|
|
|
|
if markErr := service.markBootstrapMaterializationFailed(ctx, trimmedInstallationID, failure); markErr != nil {
|
|
return errors.Join(err, markErr)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
return service.markBootstrapMaterializationSucceeded(ctx, trimmedInstallationID)
|
|
}
|
|
|
|
func (service *Service) materializeBootstrapStructure(ctx context.Context, installation InstallationRecord) error {
|
|
admin, err := service.GetAdmin(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if admin == nil {
|
|
return ErrAdminNotConfigured
|
|
}
|
|
|
|
organization, err := service.loadPrimaryOrganization(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if organization == nil {
|
|
return ErrBootstrapStructureMissing
|
|
}
|
|
|
|
department, err := service.loadPrimaryDepartment(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if department == nil {
|
|
return ErrBootstrapStructureMissing
|
|
}
|
|
|
|
team, err := service.loadPrimaryTeam(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if team == nil {
|
|
return ErrBootstrapStructureMissing
|
|
}
|
|
|
|
project, err := service.loadPrimaryProject(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if project == nil {
|
|
return ErrBootstrapStructureMissing
|
|
}
|
|
|
|
if err := service.ensureBootstrapPOSIXSkeleton(
|
|
installation,
|
|
AdminSummary{ID: admin.ID, Email: admin.Email, DisplayName: admin.DisplayName},
|
|
namedRecord{ID: organization.ID, Name: organization.Name, Slug: organization.Slug},
|
|
namedRecord{ID: department.ID, Name: department.Name, Slug: department.Slug},
|
|
namedRecord{ID: team.ID, Name: team.Name, Slug: team.Slug},
|
|
namedRecord{ID: project.ID, Name: project.Name, Slug: project.Slug},
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
return service.rebuildProjection(ctx)
|
|
}
|
|
|
|
func (service *Service) rebuildProjection(ctx context.Context) error {
|
|
if err := posixproj.NewProjector(service.db, service.posixRoot).Rebuild(ctx); err != nil {
|
|
return fmt.Errorf("rebuild POSIX projection: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (service *Service) markBootstrapMaterializationRunning(ctx context.Context, installationID string) error {
|
|
_, err := service.db.Pool.Exec(ctx, `
|
|
UPDATE installations
|
|
SET
|
|
materialization_status = 'running'::bootstrap_materialization_status,
|
|
materialization_error = NULL,
|
|
materialization_started_at = NOW(),
|
|
materialization_finished_at = NULL,
|
|
updated_at = NOW()
|
|
WHERE id = $1::uuid;
|
|
`, strings.TrimSpace(installationID))
|
|
|
|
return err
|
|
}
|
|
|
|
func (service *Service) markBootstrapMaterializationSucceeded(ctx context.Context, installationID string) error {
|
|
_, err := service.db.Pool.Exec(ctx, `
|
|
UPDATE installations
|
|
SET
|
|
materialization_status = 'succeeded'::bootstrap_materialization_status,
|
|
materialization_error = NULL,
|
|
materialization_finished_at = NOW(),
|
|
updated_at = NOW()
|
|
WHERE id = $1::uuid;
|
|
`, strings.TrimSpace(installationID))
|
|
|
|
return err
|
|
}
|
|
|
|
func (service *Service) markBootstrapMaterializationFailed(ctx context.Context, installationID, failure string) error {
|
|
_, err := service.db.Pool.Exec(ctx, `
|
|
UPDATE installations
|
|
SET
|
|
materialization_status = 'failed'::bootstrap_materialization_status,
|
|
materialization_error = $2,
|
|
materialization_finished_at = NOW(),
|
|
updated_at = NOW()
|
|
WHERE id = $1::uuid;
|
|
`, strings.TrimSpace(installationID), strings.TrimSpace(failure))
|
|
|
|
return err
|
|
}
|