package provider import ( "errors" "fmt" "maps" "path" "sync" "time" "github.com/rs/zerolog" "github.com/yusing/godoxy/agent/pkg/agent" "github.com/yusing/godoxy/internal/docker" "github.com/yusing/godoxy/internal/route" provider "github.com/yusing/godoxy/internal/route/provider/types" "github.com/yusing/godoxy/internal/types" W "github.com/yusing/godoxy/internal/watcher" watcherEvents "github.com/yusing/godoxy/internal/watcher/events" gperr "github.com/yusing/goutils/errs" "github.com/yusing/goutils/eventqueue" "github.com/yusing/goutils/events" "github.com/yusing/goutils/task" ) type ( Provider struct { ProviderImpl t provider.Type routes route.Routes routesMu sync.RWMutex watcher W.Watcher } ProviderImpl interface { fmt.Stringer ShortName() string IsExplicitOnly() bool loadRoutesImpl() (route.Routes, error) NewWatcher() W.Watcher Logger() *zerolog.Logger } ) const ( providerEventFlushInterval = 300 * time.Millisecond ) var ErrEmptyProviderName = errors.New("empty provider name") var _ types.RouteProvider = (*Provider)(nil) func newProvider(t provider.Type) *Provider { return &Provider{t: t} } func NewFileProvider(filename string) (p *Provider, err error) { name := path.Base(filename) if name == "" { return nil, ErrEmptyProviderName } p = newProvider(provider.ProviderTypeFile) p.ProviderImpl, err = FileProviderImpl(filename) if err != nil { return nil, err } p.watcher = p.NewWatcher() return p, err } func NewDockerProvider(name string, dockerCfg types.DockerProviderConfig) *Provider { p := newProvider(provider.ProviderTypeDocker) p.ProviderImpl = DockerProviderImpl(name, dockerCfg) p.watcher = p.NewWatcher() return p } func NewAgentProvider(cfg *agent.AgentConfig) *Provider { p := newProvider(provider.ProviderTypeAgent) agent := &AgentProvider{ AgentConfig: cfg, docker: DockerProviderImpl(cfg.Name, types.DockerProviderConfig{ URL: cfg.FakeDockerHost(), }), } p.ProviderImpl = agent p.watcher = p.NewWatcher() return p } func (p *Provider) GetType() provider.Type { return p.t } // MarshalText implements encoding.TextMarshaler. func (p *Provider) MarshalText() ([]byte, error) { return []byte(p.String()), nil } // Start implements task.TaskStarter. func (p *Provider) Start(parent task.Parent) error { errs := gperr.NewGroup("routes error") t := parent.Subtask("provider."+p.String(), false) // no need to lock here because we are not modifying the routes map. routeSlice := make([]*route.Route, 0, len(p.routes)) for _, r := range p.routes { routeSlice = append(routeSlice, r) } for _, r := range routeSlice { errs.Go(func() error { return p.startRoute(t, r) }) } err := errs.Wait().Error() opts := eventqueue.Options[watcherEvents.Event]{ FlushInterval: providerEventFlushInterval, OnFlush: func(evs []watcherEvents.Event) { handler := p.newEventHandler() // routes' lifetime should follow the provider's lifetime handler.Handle(t, evs) handler.Log() globalEvents := make([]events.Event, len(evs)) for i, ev := range evs { globalEvents[i] = events.NewEvent(events.LevelInfo, "provider_event", ev.Action.String(), map[string]any{ "provider": p.String(), "type": ev.Type, // file / docker "actor": ev.ActorName, // file path / container name }) } events.Global.AddAll(globalEvents) }, OnError: func(err error) { p.Logger().Err(err).Msg("event error") }, } eventQueue := eventqueue.New(t.Subtask("event_queue", false), opts) eventQueue.Start(p.watcher.Events(t.Context())) if err != nil { return err.Subject(p.String()) } return nil } func (p *Provider) LoadRoutes() (err error) { p.routes, err = p.loadRoutes() return err } func (p *Provider) NumRoutes() int { return len(p.routes) } func (p *Provider) IterRoutes(yield func(string, types.Route) bool) { routes := p.lockCloneRoutes() for alias, r := range routes { impl := r.Impl() if impl == nil { continue } if !yield(alias, impl) { break } } } func (p *Provider) FindService(project, service string) (types.Route, bool) { switch p.GetType() { case provider.ProviderTypeDocker, provider.ProviderTypeAgent: default: return nil, false } if project == "" || service == "" { return nil, false } routes := p.lockCloneRoutes() for _, r := range routes { cont := r.ContainerInfo() if docker.DockerComposeProject(cont) != project { continue } if docker.DockerComposeService(cont) == service { return r.Impl(), true } } return nil, false } func (p *Provider) GetRoute(alias string) (types.Route, bool) { r, ok := p.lockGetRoute(alias) if !ok { return nil, false } return r.Impl(), true } func (p *Provider) loadRoutes() (routes route.Routes, err error) { routes, err = p.loadRoutesImpl() if err != nil && len(routes) == 0 { return route.Routes{}, err } errs := gperr.NewBuilder("routes error") errs.Add(err) // check for exclusion // set alias and provider, then validate for alias, r := range routes { r.Alias = alias r.SetProvider(p) if err := r.Validate(); err != nil { errs.AddSubject(err, alias) delete(routes, alias) continue } r.FinalizeHomepageConfig() } return routes, errs.Error() } func (p *Provider) startRoute(parent task.Parent, r *route.Route) error { err := r.Start(parent) if err != nil { p.lockDeleteRoute(r.Alias) return gperr.PrependSubject(err, r.Alias) } p.lockAddRoute(r) if !r.ShouldExclude() { r.Task().OnCancel("remove_route_from_provider", func() { p.lockDeleteRoute(r.Alias) }) } return nil } func (p *Provider) lockAddRoute(r *route.Route) { p.routesMu.Lock() defer p.routesMu.Unlock() p.routes[r.Alias] = r } func (p *Provider) lockDeleteRoute(alias string) { p.routesMu.Lock() defer p.routesMu.Unlock() delete(p.routes, alias) } func (p *Provider) lockGetRoute(alias string) (*route.Route, bool) { p.routesMu.RLock() defer p.routesMu.RUnlock() r, ok := p.routes[alias] return r, ok } func (p *Provider) lockCloneRoutes() route.Routes { p.routesMu.RLock() defer p.routesMu.RUnlock() return maps.Clone(p.routes) }