feat: add idempotent calendar bootstrap
Bootstrap independently reviewed catalogs through audited import, approval, and initial activation transitions, then resume insight scheduling safely.
This commit is contained in:
140
internal/eventcatalog/bootstrap.go
Normal file
140
internal/eventcatalog/bootstrap.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package eventcatalog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCatalogNotFound = errors.New("event catalog: version not found")
|
||||
ErrCatalogConflict = errors.New("event catalog: bootstrap conflict")
|
||||
)
|
||||
|
||||
type CatalogStatus string
|
||||
|
||||
const (
|
||||
CatalogStatusDraft CatalogStatus = "DRAFT"
|
||||
CatalogStatusApproved CatalogStatus = "APPROVED"
|
||||
CatalogStatusActive CatalogStatus = "ACTIVE"
|
||||
CatalogStatusRetired CatalogStatus = "RETIRED"
|
||||
)
|
||||
|
||||
type CatalogState struct {
|
||||
Version string
|
||||
Status CatalogStatus
|
||||
CoverageStartJalaliYear int
|
||||
CoverageEndJalaliYear int
|
||||
SourceURI string
|
||||
SourceVersion string
|
||||
SourceChecksum string
|
||||
ImportedBy string
|
||||
}
|
||||
|
||||
type BootstrapResult struct {
|
||||
Version string
|
||||
Status CatalogStatus
|
||||
Transitions []string
|
||||
NoOp bool
|
||||
CreatedJobs int
|
||||
}
|
||||
|
||||
type bootstrapStore interface {
|
||||
CatalogState(context.Context, string) (CatalogState, error)
|
||||
ActiveCatalogState(context.Context) (CatalogState, error)
|
||||
ImportDraft(context.Context, Import) error
|
||||
Approve(context.Context, string, string) error
|
||||
ActivateInitial(context.Context, string, string) error
|
||||
}
|
||||
|
||||
type BootstrapScheduler func(context.Context) (int, error)
|
||||
|
||||
func BootstrapCatalog(ctx context.Context, store bootstrapStore, input Import, currentJalaliYear int, reviewer, operator string, schedule BootstrapScheduler) (BootstrapResult, error) {
|
||||
reviewer = strings.TrimSpace(reviewer)
|
||||
operator = strings.TrimSpace(operator)
|
||||
if err := ValidateImport(input, currentJalaliYear); err != nil {
|
||||
return BootstrapResult{}, err
|
||||
}
|
||||
if err := ValidateReviewableChecksum(input.SourceChecksum); err != nil {
|
||||
return BootstrapResult{}, err
|
||||
}
|
||||
if err := ValidateApproval(input.ImportedBy, reviewer); err != nil {
|
||||
return BootstrapResult{}, err
|
||||
}
|
||||
if operator == "" {
|
||||
return BootstrapResult{}, errors.New("event catalog: activation operator is required")
|
||||
}
|
||||
if schedule == nil {
|
||||
return BootstrapResult{}, errors.New("event catalog: bootstrap scheduler is required")
|
||||
}
|
||||
|
||||
active, err := store.ActiveCatalogState(ctx)
|
||||
if err != nil && !errors.Is(err, ErrCatalogNotFound) {
|
||||
return BootstrapResult{}, fmt.Errorf("loading active calendar catalog: %w", err)
|
||||
}
|
||||
if active.Version != "" && active.Version != input.Version {
|
||||
return BootstrapResult{}, fmt.Errorf("%w: active version %q differs from requested version %q", ErrCatalogConflict, active.Version, input.Version)
|
||||
}
|
||||
|
||||
imported := false
|
||||
state, err := store.CatalogState(ctx, input.Version)
|
||||
if errors.Is(err, ErrCatalogNotFound) {
|
||||
if err := store.ImportDraft(ctx, input); err != nil {
|
||||
return BootstrapResult{}, fmt.Errorf("importing calendar draft: %w", err)
|
||||
}
|
||||
state = stateFromImport(input, CatalogStatusDraft)
|
||||
imported = true
|
||||
} else if err != nil {
|
||||
return BootstrapResult{}, fmt.Errorf("loading calendar catalog version: %w", err)
|
||||
} else if !state.matches(input) {
|
||||
return BootstrapResult{}, fmt.Errorf("%w: version %q metadata does not match the reviewed file", ErrCatalogConflict, input.Version)
|
||||
}
|
||||
|
||||
result := BootstrapResult{Version: input.Version}
|
||||
if imported {
|
||||
result.Transitions = append(result.Transitions, "IMPORTED")
|
||||
}
|
||||
if state.Status == CatalogStatusDraft && active.Version == "" {
|
||||
if err := store.Approve(ctx, input.Version, reviewer); err != nil {
|
||||
return BootstrapResult{}, fmt.Errorf("approving calendar catalog: %w", err)
|
||||
}
|
||||
result.Transitions = append(result.Transitions, "APPROVED")
|
||||
state.Status = CatalogStatusApproved
|
||||
}
|
||||
if state.Status == CatalogStatusApproved && active.Version == "" {
|
||||
if err := store.ActivateInitial(ctx, input.Version, operator); err != nil {
|
||||
return BootstrapResult{}, fmt.Errorf("activating calendar catalog: %w", err)
|
||||
}
|
||||
result.Transitions = append(result.Transitions, "ACTIVATED")
|
||||
state.Status = CatalogStatusActive
|
||||
}
|
||||
if state.Status != CatalogStatusActive {
|
||||
return BootstrapResult{}, fmt.Errorf("%w: version %q has status %q", ErrCatalogConflict, input.Version, state.Status)
|
||||
}
|
||||
created, err := schedule(ctx)
|
||||
if err != nil {
|
||||
return BootstrapResult{}, fmt.Errorf("scheduling calendar refresh: %w", err)
|
||||
}
|
||||
result.Status = CatalogStatusActive
|
||||
result.CreatedJobs = created
|
||||
result.NoOp = len(result.Transitions) == 0 && created == 0
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func stateFromImport(input Import, status CatalogStatus) CatalogState {
|
||||
return CatalogState{
|
||||
Version: input.Version,
|
||||
Status: status,
|
||||
CoverageStartJalaliYear: input.CoverageStartJalaliYear,
|
||||
CoverageEndJalaliYear: input.CoverageEndJalaliYear,
|
||||
SourceURI: input.SourceURI,
|
||||
SourceVersion: input.SourceVersion,
|
||||
SourceChecksum: input.SourceChecksum,
|
||||
ImportedBy: input.ImportedBy,
|
||||
}
|
||||
}
|
||||
|
||||
func (state CatalogState) matches(input Import) bool {
|
||||
return state == stateFromImport(input, state.Status)
|
||||
}
|
||||
Reference in New Issue
Block a user