Bootstrap independently reviewed catalogs through audited import, approval, and initial activation transitions, then resume insight scheduling safely.
141 lines
4.8 KiB
Go
141 lines
4.8 KiB
Go
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)
|
|
}
|