Skip to content

Commit

Permalink
feat: optimize config (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
wweir authored Sep 29, 2024
2 parents 01b7ac1 + c4f8b5f commit 3555b53
Show file tree
Hide file tree
Showing 10 changed files with 368 additions and 317 deletions.
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ test:
${GO} test ./...

build: test
${GO} build -ldflags "\
${GO} build -trimpath -ldflags "\
-X main.version=$(shell git describe --tags --always) \
-X main.date=$(shell date +%Y-%m-%d)" \
-o bin/contatto .
run: build
./bin/contatto proxy -c contatto.toml
./bin/contatto proxy --debug -c contatto.toml

clean:
rm -f ./bin/contatto
225 changes: 50 additions & 175 deletions etc/config.go
Original file line number Diff line number Diff line change
@@ -1,171 +1,116 @@
package etc

import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"log/slog"
"os"
"path/filepath"
"reflect"
"regexp"
"strings"
"sync"
"text/template"

"github.com/go-viper/mapstructure/v2"
"github.com/pelletier/go-toml/v2"
"gopkg.in/yaml.v3"
)

var (
Branch, Version, Date string
var Branch, Version, Date string

RegM = map[string]*Registry{}
RegHostM = map[string]*Registry{}
RuleHostM = map[string]*MirrorRule{}
DockerAuth = map[string]*dockerAuth{}

OnMissing func(any) (string, error)
bufferPool = sync.Pool{
New: func() any { return bytes.NewBuffer(nil) },
}
)

var Config struct {
Addr string
MirrorRegistry string
OnMissing string
onMissingTpl *template.Template
Registries []Registry
MirrorRules []MirrorRule
}

type Registry struct {
Name string
Host string
Insecure bool
type config struct {
Addr string
DockerConfigFile string
UserName string
Password string
BaseRule MirrorRule
Registry map[string]*Registry
Rule map[string]*MirrorRule
}

type dockerAuth struct {
UserName string
Password string
}

func InitConfig(file string) error {
func ReadConfig(file string) (*config, error) {
f, err := os.Open(file)
if err != nil {
slog.Error("open config file", "file", file, "err", err)
return err
return nil, err
}
defer f.Close()

var decodeFn func(any) error
decodeM := map[string]any{}
switch filepath.Ext(file) {
case ".json":
decodeFn = json.NewDecoder(f).Decode
err = json.NewDecoder(f).Decode(&decodeM)
case ".toml":
decodeFn = toml.NewDecoder(f).Decode
err = toml.NewDecoder(f).Decode(&decodeM)
case ".yaml", ".yml":
decodeFn = yaml.NewDecoder(f).Decode
err = yaml.NewDecoder(f).Decode(&decodeM)
}

decodeM := map[string]any{}
if err := decodeFn(&decodeM); err != nil {
slog.Error("failed to decode config", "err", err)
return err
if err != nil {
return nil, fmt.Errorf("decode config: %w", err)
}

c := config{}
decoder, _ := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
if f.Kind() != reflect.String || t.Kind() != reflect.String {
return data, nil
}
return ReadSHEnv(data.(string))
return c.ReadSHEnv(data.(string))
},
TagName: "json",
Result: &Config,
Result: &c,
MatchName: func(mapKey, fieldName string) bool {
return strings.EqualFold(strings.ReplaceAll(mapKey, "_", ""), fieldName)
},
})
if err := decoder.Decode(decodeM); err != nil {
slog.Error("failed to decode config", "err", err)
return err
return nil, fmt.Errorf("mapstructure config: %w", err)
}

slog.Info("Starting with config", "branch", Branch, "version", Version, "date", Date, "config", Config)
slog.Info("Starting with config", "branch", Branch, "version", Version, "date", Date, "config", c)

Config.OnMissing = strings.TrimSpace(Config.OnMissing)
if Config.OnMissing != "" {
Config.onMissingTpl, err = template.New(".").Parse(Config.OnMissing)
if err != nil {
slog.Error("failed to parse on missing", "err", err)
return err
for host, registry := range c.Registry {
if registry.registry == "" {
registry.registry = host
}

OnMissing = func(param any) (string, error) {
buf := bufferPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
bufferPool.Put(buf)
}()

if err := Config.onMissingTpl.Execute(buf, param); err != nil {
return "", err
}

return buf.String(), nil
if registry.Alias != "" {
c.Registry[registry.Alias] = registry
}

}

RegM = map[string]*Registry{}
RegHostM = map[string]*Registry{}
for i, reg := range Config.Registries {
RegM[reg.Name] = &Config.Registries[i]
RegHostM[reg.Host] = &Config.Registries[i]
if reg.DockerConfigFile != "" && reg.UserName == "" {
user, password, err := readAuthFromDockerConfig(reg.DockerConfigFile, reg.Host)
if err != nil {
slog.Error("failed to read auth from docker config", "err", err)
return err
}

Config.Registries[i].UserName = user
Config.Registries[i].Password = password
}
if err := c.BaseRule.ParseTemplate(); err != nil {
return nil, fmt.Errorf("parse base rule: %w", err)
}

RuleHostM = map[string]*MirrorRule{}
for i, rule := range Config.MirrorRules {
src, ok := RegM[rule.RawRegName]
if !ok {
slog.Error("config registry not found", "reg", rule.RawRegName)
return err
for registry, rule := range c.Rule {
if _, ok := c.Registry[registry]; !ok {
c.Registry[registry] = &Registry{registry: registry}
}

Config.MirrorRules[i].mirrorPathTpl, err = template.New(".").Parse(rule.MirrorPathTpl)
if err != nil {
slog.Error("failed to parse mirror path template", "err", err)
return err
if rule.MirrorRegistry == "" {
rule.MirrorRegistry = c.BaseRule.MirrorRegistry
}

RuleHostM[src.Host] = &Config.MirrorRules[i]
if err := rule.ParseTemplate(); err != nil {
return nil, fmt.Errorf("parse rule: %w", err)
}
if rule.PathTpl == "" {
rule.pathTpl = c.BaseRule.pathTpl
}
if rule.OnMissingTpl == "" {
rule.onMissingTpl = c.BaseRule.onMissingTpl
}
}

return nil
return &c, nil
}

var envRe = regexp.MustCompile(`\$\{([a-zA-Z0-9_]+)\}`)

func ReadSHEnv(value string) (string, error) {
func (c *config) ReadSHEnv(value string) (string, error) {
idxPairs := envRe.FindAllStringIndex(value, -1)
if len(idxPairs) == 0 {
return value, nil
}

newValue := ""
for _, idxPair := range idxPairs {
if readBeforeByte(value, idxPair[0]) == '$' {
if c.readBeforeByte(value, idxPair[0]) == '$' {
newValue += value[:idxPair[0]] + value[idxPair[0]+1:idxPair[1]]
continue
}
Expand All @@ -179,79 +124,9 @@ func ReadSHEnv(value string) (string, error) {
return newValue + value[lastIdx:], nil
}

func readBeforeByte(value string, idx int) byte {
func (c *config) readBeforeByte(value string, idx int) byte {
if idx == 0 {
return 0
}
return value[idx-1]
}

// https://github.com/docker/cli/blob/a18c896928828eca5eb91e816f009268fe0cd995/cli/config/configfile/file.go#L232
func readAuthFromDockerConfig(configFile, registryHost string) (user, password string, err error) {
f, err := os.Open(configFile)
if err != nil {
slog.Error("open docker config file", "file", configFile, "err", err)
return "", "", err
}
defer f.Close()

var dockerConfig struct {
Auths map[string]struct {
Auth string `json:"auth"`
} `json:"auths"`
}

de := json.NewDecoder(f)
if err := de.Decode(&dockerConfig); err != nil {
slog.Error("failed to decode docker config", "err", err)
return "", "", err
}

auth, ok := dockerConfig.Auths[registryHost]
if !ok {
slog.Error("registry not found in docker config", "registry", registryHost)
return "", "", err
}

authStr := auth.Auth
decLen := base64.StdEncoding.DecodedLen(len(authStr))
decoded := make([]byte, decLen)
authByte := []byte(authStr)
n, err := base64.StdEncoding.Decode(decoded, authByte)
if err != nil {
slog.Error("failed to decode auth", "registry", registryHost, "err", err)
return "", "", err
}
if n > decLen {
slog.Error("something went wrong decoding auth config", "registry", registryHost)
return "", "", err
}

userName, password, ok := strings.Cut(string(decoded), ":")
if !ok || userName == "" {
slog.Error("failed to parse auth", "registry", registryHost, "err", err)
return "", "", err
}

return userName, strings.Trim(password, "\x00"), nil
}

type MirrorRule struct {
RawRegName string
MirrorPathTpl string // rendering a image path: /wweir/alpine:latest
mirrorPathTpl *template.Template
}

func (r *MirrorRule) RenderMirrorPath(param any) (string, error) {
buf := bufferPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
bufferPool.Put(buf)
}()

if err := r.mirrorPathTpl.Execute(buf, param); err != nil {
return "", err
}

return buf.String(), nil
}
53 changes: 25 additions & 28 deletions etc/contatto.toml
Original file line number Diff line number Diff line change
@@ -1,32 +1,29 @@
# 服务端口配置
addr = "127.0.0.1:8080"
# Docker 用户配置文件,一般是 $HOME/.docker/config.json,留空则不从 docker 配置中读取用户名和密码
mirror_registry = "local-registry"
on_missing = """
ssh <PROXY_SERVER> sh -c '
docker pull {{.raw}}
docker tag {{.raw}} {{.mirror}}
docker push {{.mirror}}
docker rmi {{.raw}} {{.mirror}}'
"""
## Retrieve authentication information from the Docker configuration file if the username and password are not specified.
docker_config_file = "${HOME}/.docker/config.json"

# 镜像仓库配置MirrorRegistry = "swr"# 镜像仓库, 应当设置为自主可控的镜像仓库地址
[[Registries]]
Name = "local-registry"
# mirror registry 地址
# Host = "localhost:5000"
# 从 docker 配置文件中读取用户名和密码
DockerConfigFile = "${HOME}/.docker/config.json"
## 使用指定的用户名和密码进行身份验证,优先级高于 Docker 存储的令牌
# UserName = "my_username_1"
# Password = "my_password_1"
# if the field in rule is empty, use the value in base_rule.
[base_rule]
mirror_registry = "mirror-registry"
## Available parameters in template: {{.raw}} {{.mirror}}
## {{.Raw.Registry}} {{.Raw.Alias}} {{.Raw.Project}} {{.Raw.Repo}} {{.Raw.Tag}}
## {{.Mirror.Registry}} {{.Mirror.Alias}} {{.Mirror.Project}} {{.Mirror.Repo}} {{.Mirror.Tag}}
path_tpl = "{{.Registry}}/{{.Project}}-{{.Repo}}:{{.Tag}}"
## The cmd will be executed when the image is not found in the mirror registry.
# on_missing_tpl = """
# ssh <PROXY_SERVER> -- \
# 'docker pull {{.raw}} && docker tag {{.raw}} {{.mirror}} && docker push {{.mirror}}'
# """

[registry."<local-registry>:5000"]
alias = "mirror-registry"
# user = "my_username_1"
# password = "my_password_1"

# 镜像规则配置
[[Registries]]
Name = "docker-hub"
Host = "docker.io"

[[MirrorRules]]
RawRegName = "docker-hub"
MirrorPathTpl = "docker-hub/{{.Project}}-{{.Repo}}:{{.Tag}}"
[rule."docker.io"]
[rule."gcr.io"]
[rule."ghcr.io"]
[rule."k8s.gcr.io"]
[rule."registry.k8s.io"]
[rule."quay.io"]
[rule."nvcr.io"]
Loading

0 comments on commit 3555b53

Please sign in to comment.