package provider import ( "fmt" "strconv" "strings" "github.com/docker/docker/client" "github.com/rs/zerolog" "github.com/yusing/go-proxy/internal/common" "github.com/yusing/go-proxy/internal/docker" E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/route" U "github.com/yusing/go-proxy/internal/utils" "github.com/yusing/go-proxy/internal/utils/strutils" "github.com/yusing/go-proxy/internal/watcher" "gopkg.in/yaml.v3" ) type DockerProvider struct { name, dockerHost string l zerolog.Logger } const ( aliasRefPrefix = '#' aliasRefPrefixAlt = '$' ) var ErrAliasRefIndexOutOfRange = E.New("index out of range") func DockerProviderImpl(name, dockerHost string) (ProviderImpl, error) { if dockerHost == common.DockerHostFromEnv { dockerHost = common.GetEnvString("DOCKER_HOST", client.DefaultDockerHost) } return &DockerProvider{ name, dockerHost, logger.With().Str("type", "docker").Str("name", name).Logger(), }, nil } 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.dockerHost) } func (p *DockerProvider) loadRoutesImpl() (route.Routes, E.Error) { routes := route.NewRoutes() entries := route.NewProxyEntries() containers, err := docker.ListContainers(p.dockerHost) if err != nil { return routes, E.From(err) } errs := E.NewBuilder("") for _, c := range containers { container := docker.FromDocker(&c, p.dockerHost) if container.IsExcluded { continue } newEntries, err := p.entriesFromContainerLabels(container) if err != nil { errs.Add(err.Subject(container.ContainerName)) } // although err is not nil // there may be some valid entries in `en` dups := entries.MergeFrom(newEntries) // add the duplicate proxy entries to the error dups.RangeAll(func(k string, v *route.RawEntry) { errs.Addf("duplicated alias %s", k) }) } routes, err = route.FromEntries(p.ShortName(), entries) errs.Add(err) return routes, errs.Error() } func (p *DockerProvider) shouldIgnore(container *docker.Container) bool { return container.IsExcluded || !container.IsExplicit && p.IsExplicitOnly() || !container.IsExplicit && container.IsDatabase || strings.HasSuffix(container.ContainerName, "-old") } // Returns a list of proxy entries for a container. // Always non-nil. func (p *DockerProvider) entriesFromContainerLabels(container *docker.Container) (entries route.RawEntries, _ E.Error) { entries = route.NewProxyEntries() if p.shouldIgnore(container) { return } // init entries map for all aliases for _, a := range container.Aliases { entries.Store(a, &route.RawEntry{ Alias: a, Container: container, }) } errs := E.NewBuilder("label errors") m, err := docker.ParseLabels(container.Labels) errs.Add(err) var wildcardProps docker.LabelMap for alias, entryMapAny := range m { if len(alias) == 0 { errs.Add(E.New("empty alias")) continue } entryMap, ok := entryMapAny.(docker.LabelMap) if !ok { // try to deserialize to map entryMap = make(docker.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.Add(E.From(err).Subject(alias)) continue } } if alias == docker.WildcardAlias { wildcardProps = entryMap continue } // check if it is an alias reference switch alias[0] { case aliasRefPrefix, aliasRefPrefixAlt: index, err := strutils.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 en, ok := entries.Load(alias) if !ok { en = &route.RawEntry{ Alias: alias, Container: container, } entries.Store(alias, en) } // deserialize map into entry object err := U.Deserialize(entryMap, en) if err != nil { errs.Add(err.Subject(alias)) } else { entries.Store(alias, en) } } if wildcardProps != nil { entries.Range(func(alias string, re *route.RawEntry) bool { if err := U.Deserialize(wildcardProps, re); err != nil { errs.Add(err.Subject(docker.WildcardAlias)) return false } return true }) } return entries, errs.Error() }