package provider import ( "context" "fmt" "strconv" "time" "github.com/goccy/go-yaml" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/yusing/godoxy/internal/docker" "github.com/yusing/godoxy/internal/route" "github.com/yusing/godoxy/internal/serialization" "github.com/yusing/godoxy/internal/types" "github.com/yusing/godoxy/internal/watcher" gperr "github.com/yusing/goutils/errs" ) type DockerProvider struct { name string dockerCfg types.DockerProviderConfig l zerolog.Logger } const ( aliasRefPrefix = '#' aliasRefPrefixAlt = '$' ) var ErrAliasRefIndexOutOfRange = gperr.New("index out of range") func DockerProviderImpl(name string, dockerCfg types.DockerProviderConfig) ProviderImpl { return &DockerProvider{ name, dockerCfg, log.With().Str("type", "docker").Str("name", name).Logger(), } } func (p *DockerProvider) String() string { return "docker@" + p.name } func (p *DockerProvider) ShortName() string { return p.name } func (p *DockerProvider) IsExplicitOnly() bool { return p.name[len(p.name)-1] == '!' } func (p *DockerProvider) Logger() *zerolog.Logger { return &p.l } func (p *DockerProvider) NewWatcher() watcher.Watcher { return watcher.NewDockerWatcher(p.dockerCfg) } func (p *DockerProvider) loadRoutesImpl() (route.Routes, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() containers, err := docker.ListContainers(ctx, p.dockerCfg) if err != nil { return nil, err } errs := gperr.NewBuilder("") routes := make(route.Routes) for _, c := range containers { container := docker.FromDocker(&c, p.dockerCfg) if container.Errors != nil { errs.AddSubject(container.Errors, container.ContainerName) continue } if container.IsHostNetworkMode { err := docker.UpdatePorts(ctx, container) if err != nil { errs.AddSubject(err, container.ContainerName) continue } } newEntries, err := p.routesFromContainerLabels(container) if err != nil { errs.AddSubject(err, container.ContainerName) } for k, v := range newEntries { if conflict, ok := routes[k]; ok { err := gperr.Multiline(). Addf("route with alias %s already exists", k). Addf("container %s", container.ContainerName). Addf("conflicting container %s", conflict.Container.ContainerName) if conflict.ShouldExclude() || v.ShouldExclude() { log.Warn().Err(err).Msg("skipping conflicting route") } else { errs.Add(err) } } else { routes[k] = v } } } return routes, errs.Error() } // Returns a list of proxy entries for a container. // Always non-nil. func (p *DockerProvider) routesFromContainerLabels(container *types.Container) (route.Routes, error) { if !container.IsExplicit && p.IsExplicitOnly() { return make(route.Routes, 0), nil } routes := make(route.Routes, len(container.Aliases)) // init entries map for all aliases for _, a := range container.Aliases { routes[a] = &route.Route{ Alias: a, Metadata: route.Metadata{ Container: container, }, } } errs := gperr.NewBuilder("label errors") m, err := docker.ParseLabels(container.Labels, container.Aliases...) errs.Add(err) for alias, entryMapAny := range m { if len(alias) == 0 { errs.Adds("empty alias") continue } entryMap, ok := entryMapAny.(types.LabelMap) if !ok { // try to deserialize to map entryMap = make(types.LabelMap) yamlStr, ok := entryMapAny.(string) if !ok { // should not happen panic(fmt.Errorf("invalid entry map type %T", entryMapAny)) } if err := yaml.Unmarshal([]byte(yamlStr), &entryMap); err != nil { errs.AddSubject(err, alias) continue } } // check if it is an alias reference switch alias[0] { case aliasRefPrefix, aliasRefPrefixAlt: index, err := strconv.Atoi(alias[1:]) if err != nil { errs.Add(err) break } if index < 1 || index > len(container.Aliases) { errs.Add(ErrAliasRefIndexOutOfRange.Subject(strconv.Itoa(index))) break } alias = container.Aliases[index-1] } // init entry if not exist r, ok := routes[alias] if !ok { r = &route.Route{ Alias: alias, Metadata: route.Metadata{ Container: container, }, } routes[alias] = r } // deserialize map into entry object err := serialization.MapUnmarshalValidate(entryMap, r) if err != nil { errs.AddSubject(err, alias) } else { routes[alias] = r } } return routes, errs.Error() }