Skip to content

Commit

Permalink
support containerd and docker mirror registry (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
wweir authored Sep 17, 2024
2 parents 8a4981c + dbb09e8 commit 90a82eb
Show file tree
Hide file tree
Showing 11 changed files with 705 additions and 0 deletions.
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,14 @@
# Go workspace file
go.work
go.work.sum

# Customize
.DS_Store
._*
*.swp
*.swo
.vscode
.idea
**.env
/bin
/config.toml
19 changes: 19 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
MAKEFLAGS += --jobs all
GO:=CGO_ENABLED=0 GODEBUG=httpmuxgo121=1 go

default: test build

test:
${GO} vet ./...
${GO} test ./...

build: test
${GO} build -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 config.toml

clean:
rm -f ./bin/contatto
210 changes: 210 additions & 0 deletions conf/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package conf

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

"github.com/BurntSushi/toml"
"gopkg.in/yaml.v3"
)

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
DockerConfigFile string
UserName string
Password string
}

type dockerAuth struct {
UserName string
Password string
}

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

switch filepath.Ext(file) {
case ".json":
if err := json.NewDecoder(f).Decode(&Config); err != nil {
log.Fatal(err)
}
case ".toml":
if _, err := toml.NewDecoder(f).Decode(&Config); err != nil {
log.Fatal(err)
}
case ".yaml", ".yml":
if err := yaml.NewDecoder(f).Decode(&Config); err != nil {
log.Fatal(err)
}
}

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

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
}

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
}

}

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
}
}

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
}

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
}

RuleHostM[src.Host] = &Config.MirrorRules[i]
}

return nil
}

// 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
}
31 changes: 31 additions & 0 deletions conf/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# 服务端口配置
Addr = "127.0.0.1:8080"
# Docker 用户配置文件,一般是 $HOME/.docker/config.json,留空则不从 docker 配置中读取用户名和密码
MirrorRegistry = "local-registry"
OnMissing = """
ssh <PROXY_SERVER> sh -c '
docker pull {{.raw}}
docker tag {{.raw}} {{.mirror}}
docker push {{.mirror}}
docker rmi {{.raw}} {{.mirror}}'
"""

# 镜像仓库配置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"

[[Registries]]
Name = "docker-hub"
Host = "docker.io"

# 镜像规则配置
[[MirrorRules]]
RawRegName = "docker-hub"
MirrorPathTpl = "wweir/{{.Project}}-{{.Repo}}:{{.Tag}}"
17 changes: 17 additions & 0 deletions conf/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Addr: "127.0.0.1:8080"
MirrorRegistry: local-registry
OnMissing: |
ssh <PROXY_SERVER> sh -c '
docker pull {{.raw}}
docker tag {{.raw}} {{.mirror}}
docker push {{.mirror}}
docker rmi {{.raw}} {{.mirror}}'
Registries:
- Name: local-registry
Host: "localhost:5000"
DockerConfigFile: <HOME>/.docker/config.json
- Name: docker-hub
Host: docker.io
MirrorRules:
- RawRegName: docker-hub
MirrorPathTpl: "wweir/{{.Project}}-{{.Repo}}:{{.Tag}}"
26 changes: 26 additions & 0 deletions config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# 服务端口配置
Addr = "127.0.0.1:8080"
# Docker 用户配置文件,一般是 $HOME/.docker/config.json,留空则不从 docker 配置中读取用户名和密码
MirrorRegistry = "swr"
OnMissing = """
ssh bwg sh -c 'docker pull {{.raw}} && docker tag {{.raw}} {{.mirror}} && docker push {{.mirror}}'
"""

# 镜像仓库配置MirrorRegistry = "swr"# 镜像仓库, 应当设置为自主可控的镜像仓库地址
[[Registries]]
Name = "swr"
Host = "swr.cn-east-3.myhuaweicloud.com"
## 从 docker 配置文件中读取用户名和密码
DockerConfigFile = "/home/wweir/.docker/config.json"
## 使用指定的用户名和密码进行身份验证,优先级高于 Docker 存储的令牌
# UserName = "my_username_1"
# Password = "my_password_1"

[[Registries]]
Name = "docker-hub"
Host = "docker.io"

# 镜像规则配置
[[MirrorRules]]
RawRegName = "docker-hub"
MirrorPathTpl = "wweir/{{.Project}}-{{.Repo}}:{{.Tag}}"
36 changes: 36 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module github.com/wweir/contatto

go 1.23

toolchain go1.23.0

require (
github.com/BurntSushi/toml v1.4.0
github.com/alecthomas/kong v1.2.1
github.com/containerd/containerd/v2 v2.0.0-rc.4
github.com/julienschmidt/httprouter v1.3.0
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect
github.com/containerd/errdefs v0.2.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect
go.opentelemetry.io/otel v1.30.0 // indirect
go.opentelemetry.io/otel/metric v1.30.0 // indirect
go.opentelemetry.io/otel/trace v1.30.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.25.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/grpc v1.66.2 // indirect
)
Loading

0 comments on commit 90a82eb

Please sign in to comment.