proxy-pool/internal/controller/bootstrap/bootstrap.go
2026-07-30 12:30:42 +08:00

193 lines
6.1 KiB
Go

package bootstrap
import (
"context"
"errors"
"fmt"
"reflect"
"strings"
"time"
"github.com/prometheus/client_golang/prometheus"
"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"
platformMetrics "proxy-pool/internal/platform/metrics"
)
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
metricsReadiness platformMetrics.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
}
if loaded.Value.Metrics.Enabled {
if nilInterface(opened.metricsReadiness) {
return errors.Join(ErrStartup, ErrInvalidOptions)
}
handler, handlerErr := platformMetrics.NewHandler(platformMetrics.Dependencies{
Gatherer: prometheus.DefaultGatherer, Readiness: opened.metricsReadiness,
})
if handlerErr != nil {
return fmt.Errorf("%w: build metrics handler: %w", ErrStartup, handlerErr)
}
dependencies.MetricsHandler = handler
}
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)
}