package bootstrap import ( "context" "errors" "fmt" "reflect" "strings" "time" "proxy-pool/internal/config" "proxy-pool/internal/controller/admin" "proxy-pool/internal/controller/distribution" "proxy-pool/internal/controller/extraction" "proxy-pool/internal/controller/operations" controllerRuntime "proxy-pool/internal/controller/runtime" "proxy-pool/internal/domain/activitypool" extractionDomain "proxy-pool/internal/domain/extraction" "proxy-pool/internal/platform/admission" "proxy-pool/internal/platform/httpserver" ) var ( ErrInvalidOptions = errors.New("invalid controller bootstrap options") ErrStartup = errors.New("controller startup failed") ) type Options struct { ConfigPath string Resolver config.Resolver Now func() time.Time HTTP httpserver.Options } type activityStore interface { extractionDomain.Store activitypool.StateInventoryReader } type ports struct { state admin.StateRepository activity activityStore readiness distribution.ReadinessChecker close func() error } type infrastructure interface { Open(context.Context, *config.Config) (ports, error) } type controllerRunner interface { Run(context.Context) error } type runtimeFactory interface { New(*config.Config, controllerRuntime.Dependencies, controllerRuntime.Options) (controllerRunner, error) } func Run(ctx context.Context, options Options) error { return run(ctx, options, &productionInfrastructure{}, productionRuntimeFactory{}) } func run(ctx context.Context, options Options, infrastructure infrastructure, factory runtimeFactory) (resultErr error) { if ctx == nil || strings.TrimSpace(options.ConfigPath) != options.ConfigPath || options.ConfigPath == "" || nilInterface(options.Resolver) || nilInterface(infrastructure) || nilInterface(factory) { return ErrInvalidOptions } if err := ctx.Err(); err != nil { return err } if options.Now == nil { options.Now = time.Now } loader, err := admin.NewFileConfigurationLoader(options.ConfigPath, options.Resolver) if err != nil { return errors.Join(ErrInvalidOptions, err) } loaded, err := loader.LoadConfiguration(ctx) if err != nil { return fmt.Errorf("%w: load configuration: %w", ErrStartup, err) } configurationStore, err := config.NewStore(loaded.Value) if err != nil { return fmt.Errorf("%w: initialize configuration store: %w", ErrStartup, err) } opened, err := infrastructure.Open(ctx, loaded.Value) if err != nil { return fmt.Errorf("%w: open infrastructure: %w", ErrStartup, err) } if opened.close == nil { return errors.Join(ErrStartup, ErrInvalidOptions) } defer func() { resultErr = errors.Join(resultErr, opened.close()) }() dependencies := controllerRuntime.Dependencies{} if loaded.Value.Distribution.Enabled { if nilInterface(opened.activity) || nilInterface(opened.readiness) { return errors.Join(ErrStartup, ErrInvalidOptions) } service, serviceErr := extraction.NewService(opened.activity, extractionPolicy(loaded.Value), admission.AllowAll{}, options.Now) if serviceErr != nil { return fmt.Errorf("%w: build extraction service: %w", ErrStartup, serviceErr) } dependencies.Extractor = service dependencies.Readiness = opened.readiness } if loaded.Value.Admin.Enabled { if nilInterface(opened.state) || nilInterface(opened.activity) { return errors.Join(ErrStartup, ErrInvalidOptions) } statusReader, statusErr := operations.NewReader(configurationStore, opened.activity, options.Now) if statusErr != nil { return fmt.Errorf("%w: build operational status reader: %w", ErrStartup, statusErr) } service, serviceErr := admin.NewApplicationService(admin.ApplicationDependencies{ State: opened.state, Operations: statusReader, Configuration: loader, Publisher: configurationStore, }, admin.ApplicationOptions{Now: options.Now}) if serviceErr != nil { return fmt.Errorf("%w: build admin service: %w", ErrStartup, serviceErr) } if _, applyErr := service.ApplyConfiguration(ctx, admin.ReloadCommand{ RequestID: "controller-startup", ActorID: "proxy-controller", }, loaded); applyErr != nil { return fmt.Errorf("%w: commit startup configuration: %w", ErrStartup, applyErr) } dependencies.AdminService = service } runner, err := factory.New(configurationStore.Current(), dependencies, controllerRuntime.Options{HTTP: options.HTTP}) if err != nil { return fmt.Errorf("%w: build HTTP runtime: %w", ErrStartup, err) } if nilInterface(runner) { return errors.Join(ErrStartup, ErrInvalidOptions) } return runner.Run(ctx) } func extractionPolicy(configuration *config.Config) extraction.Policy { configured := configuration.Distribution.Extraction return extraction.Policy{ MaxCountPerRequest: configured.MaxCountPerRequest, DefaultFulfillment: extractionDomain.Fulfillment(configured.Fulfillment), MinRemainingTTL: configured.MinRemainingTTL.Value(), MaxHealthCheckAge: configured.MaxHealthCheckAge.Value(), ReserveForGateway: configured.ReserveForGateway, IdempotencyTTL: configured.IdempotencyTTL.Value(), } } func nilInterface(value any) bool { if value == nil { return true } reflected := reflect.ValueOf(value) switch reflected.Kind() { case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice: return reflected.IsNil() default: return false } } type productionRuntimeFactory struct{} func (productionRuntimeFactory) New( configuration *config.Config, dependencies controllerRuntime.Dependencies, options controllerRuntime.Options, ) (controllerRunner, error) { return controllerRuntime.New(configuration, dependencies, options) }