mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-21 07:51:28 +02:00
This is a large-scale refactoring across the codebase that replaces the custom `gperr.Error` type with Go's standard `error` interface. The changes include: - Replacing `gperr.Error` return types with `error` in function signatures - Using `errors.New()` and `fmt.Errorf()` instead of `gperr.New()` and `gperr.Errorf()` - Using `%w` format verb for error wrapping instead of `.With()` method - Replacing `gperr.Subject()` calls with `gperr.PrependSubject()` - Converting error logging from `gperr.Log*()` functions to zerolog's `.Err().Msg()` pattern - Update NewLogger to handle multiline error message - Updating `goutils` submodule to latest commit This refactoring aligns with Go idioms and removes the dependency on custom error handling abstractions in favor of standard library patterns.
253 lines
5.5 KiB
Go
253 lines
5.5 KiB
Go
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"
|
|
"github.com/yusing/godoxy/internal/watcher/events"
|
|
gperr "github.com/yusing/goutils/errs"
|
|
"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
|
|
}
|
|
|
|
// to work with json marshaller.
|
|
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()
|
|
|
|
eventQueue := events.NewEventQueue(
|
|
t.Subtask("event_queue", false),
|
|
providerEventFlushInterval,
|
|
func(events []events.Event) {
|
|
handler := p.newEventHandler()
|
|
// routes' lifetime should follow the provider's lifetime
|
|
handler.Handle(t, events)
|
|
handler.Log()
|
|
},
|
|
func(err error) {
|
|
p.Logger().Err(err).Msg("event error")
|
|
},
|
|
)
|
|
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)
|
|
}
|