Skip to content

Commit

Permalink
feat: rename config package
Browse files Browse the repository at this point in the history
  • Loading branch information
wweir committed Sep 29, 2024
1 parent a072f7b commit 0d0a298
Show file tree
Hide file tree
Showing 8 changed files with 65 additions and 57 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ jobs:
- uses: actions/checkout@v4
- name: build matrix
run: |
mkdir -p etc
mv conf/contatto.toml etc/contatto.toml
make build GO='GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go'
tar czvf contatto-linux-amd64.tar.gz ./bin/ ./etc/contatto.toml
make clean
Expand Down
17 changes: 10 additions & 7 deletions etc/config.go → conf/config.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package etc
package conf

import (
"encoding/json"
Expand All @@ -17,15 +17,15 @@ import (

var Branch, Version, Date string

type config struct {
type Config struct {
Addr string
DockerConfigFile string
BaseRule MirrorRule
Registry map[string]*Registry
Rule map[string]*MirrorRule
}

func ReadConfig(file string) (*config, error) {
func ReadConfig(file string) (*Config, error) {
f, err := os.Open(file)
if err != nil {
return nil, err
Expand All @@ -45,7 +45,7 @@ func ReadConfig(file string) (*config, error) {
return nil, fmt.Errorf("decode config: %w", err)
}

c := config{}
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 {
Expand All @@ -69,7 +69,10 @@ func ReadConfig(file string) (*config, error) {
if registry.registry == "" {
registry.registry = host
}
if registry.Alias != "" {

if registry.Alias == "" {
registry.Alias = host
} else {
c.Registry[registry.Alias] = registry
}
}
Expand Down Expand Up @@ -102,7 +105,7 @@ func ReadConfig(file string) (*config, error) {

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

func (c *config) 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
Expand All @@ -124,7 +127,7 @@ func (c *config) ReadSHEnv(value string) (string, error) {
return newValue + value[lastIdx:], nil
}

func (c *config) readBeforeByte(value string, idx int) byte {
func (c *Config) readBeforeByte(value string, idx int) byte {
if idx == 0 {
return 0
}
Expand Down
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion etc/mirror_rule.go → conf/mirror_rule.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package etc
package conf

import (
"bytes"
Expand Down
2 changes: 1 addition & 1 deletion etc/registry.go → conf/registry.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package etc
package conf

import (
"encoding/base64"
Expand Down
14 changes: 10 additions & 4 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import (

"github.com/alecthomas/kong"
"github.com/lmittmann/tint"
"github.com/wweir/contatto/etc"
"github.com/wweir/contatto/conf"
)

var cli struct {
Debug bool `help:"debug mode"`
Config string `short:"c" required:"" default:"/etc/contatto.toml"`
Debug bool `help:"debug mode"`

Install *InstallCmd `cmd:"" help:"install contatto"`
Proxy *ProxyCmd `cmd:"" help:"run as registry proxy"`
Expand All @@ -26,9 +27,14 @@ func main() {

ctx := kong.Parse(&cli,
kong.UsageOnError(),
kong.Description(fmt.Sprintf(`Contatto %s (%s %s)`, etc.Version, etc.Branch, etc.Date)),
kong.Description(fmt.Sprintf(`Contatto %s (%s %s)`, conf.Version, conf.Branch, conf.Date)),
)
if err := ctx.Run(); err != nil {

config, err := conf.ReadConfig(cli.Config)
if err != nil {
log.Fatalln("failed to read config:", err)
}
if err := ctx.Run(config); err != nil {
log.Fatalf("run failed: %v\n", err)
}
}
84 changes: 40 additions & 44 deletions proxy.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package main

import (
"context"
"fmt"
"log/slog"
"net/http"
Expand All @@ -13,28 +12,17 @@ import (

"github.com/containerd/containerd/v2/core/remotes/docker"
"github.com/julienschmidt/httprouter"
"github.com/wweir/contatto/etc"
"github.com/wweir/contatto/conf"
)

type ProxyCmd struct {
Config string `short:"c" required:"" default:"/etc/contatto/config.toml"`

firstAttach sync.Map
firstRequest sync.Map
}

func (c *ProxyCmd) Run() error {
config, err := etc.ReadConfig(c.Config)
if err != nil {
slog.Error("failed to read config", "err", err)
return err
}

func (c *ProxyCmd) Run(config *conf.Config) error {
authorizer := docker.NewDockerAuthorizer(
docker.WithAuthCreds(func(host string) (string, string, error) {
return config.Registry[host].ReadAuthFromDockerConfig(config.DockerConfigFile)
}),
docker.WithFetchRefreshToken(func(ctx context.Context, refreshToken string, req *http.Request) {
slog.Info("fetch refresh token", "refreshToken", refreshToken, "url", req.URL.String())
}))

router := httprouter.New()
Expand All @@ -50,10 +38,11 @@ func (c *ProxyCmd) Run() error {
query := r.In.URL.Query()
host := query.Get("ns")
if host == "" {
// for docker mirror, use docker.io as default registry
host = "docker.io"
}

log := slog.With("raw_reg", host)
slog := slog.With("raw_reg", host)

rule, ok := config.Rule[host]
if !ok { // no mapping rule, directly forward to the registry
Expand All @@ -65,25 +54,23 @@ func (c *ProxyCmd) Run() error {
r.Out.URL.Scheme = "https"
}
}
log.Warn("no mapping rule")
slog.Warn("no mapping rule")
return
}

// rewrite host, scheme, query
// rewrite host, scheme, query values
dstReg := config.Registry[rule.MirrorRegistry]
r.Out.URL.Scheme = dstReg.Scheme()
r.Out.Host = dstReg.Host()
r.Out.URL.Host = r.Out.Host
query.Set("ns", r.Out.Host)
r.Out.URL.RawQuery = query.Encode()

// rewrite path, follow the mapping rule
// rewrite path and tag, rendering path template
_, ps, _ := router.Lookup(r.Out.Method, r.Out.URL.Path)
if len(ps) == 0 {
switch r.Out.URL.Path {
case "/v2/":
default:
log.Error("rewrite missing", "method", r.Out.Method, "url", r.Out.URL.String(), "ps", ps)
if r.Out.URL.Path != "/v2/" {
slog.Error("rewrite missing", "method", r.Out.Method, "url", r.Out.URL.String(), "ps", ps)
}
return
}
Expand All @@ -94,41 +81,46 @@ func (c *ProxyCmd) Run() error {
srcImage.ParseParams(ps)
mirrorPath, err := rule.RenderMirrorPath(srcImage)
if err != nil {
log.Error("failed to render mirror path", "err", err)
slog.Error("failed to render mirror path", "err", err)
return
}

dstImage.ParseImage(r.Out.Host + "/" + mirrorPath)

r.Out.URL.Path = strings.Replace(r.Out.URL.Path, srcImage.Project, dstImage.Project, 1)
r.Out.URL.Path = strings.Replace(r.Out.URL.Path, srcImage.Repo, dstImage.Repo, 1)
r.Out.URL.Path = strings.Replace(r.Out.URL.Path, srcImage.Tag, dstImage.Tag, 1)
if srcImage.Tag != "" {
r.Out.URL.Path = strings.Replace(r.Out.URL.Path, srcImage.Tag, dstImage.Tag, 1)

r.Out.Header.Set("Contatto-Raw-Image", srcImage.String())
r.Out.Header.Set("Contatto-Mirror-Image", dstImage.String())
log.Info("proxy", "mirror", dstImage)
r.Out.Header.Set("Contatto-Raw-Image", srcImage.String())
r.Out.Header.Set("Contatto-Mirror-Image", dstImage.String())

slog.Info("proxy", "mirror", dstImage)
}

// add auth header
if _, ok := c.firstAttach.LoadOrStore(dstImage.String(), struct{}{}); !ok {
if _, ok := c.firstRequest.LoadOrStore(dstImage.String(), struct{}{}); !ok {
u := *r.Out.URL
u.Path, u.RawQuery = "/v2/", ""
resp, err := http.Get(u.String())
if err != nil {
log.Error("failed to get", "err", err)
slog.Error("failed to get", "err", err)
} else {
defer resp.Body.Close()
if resp.StatusCode == 401 {
authorizer.AddResponses(r.Out.Context(), []*http.Response{resp})
}
}
}

ctx := docker.ContextWithAppendPullRepositoryScope(r.Out.Context(), dstImage.Project+"/"+dstImage.Repo)
if err := authorizer.Authorize(ctx, r.Out); err != nil {
log.Error("failed to authorize", "err", err)
slog.Error("failed to authorize", "err", err)
return
}
}
proxy.ModifyResponse = func(w *http.Response) error {
switch w.StatusCode {
case 200, 307:
case 401:
slog.Debug("auth failed", "url", w.Request.URL.String())
if err := authorizer.AddResponses(w.Request.Context(), []*http.Response{w}); err != nil {
Expand All @@ -143,31 +135,38 @@ func (c *ProxyCmd) Run() error {
})

case 404:
raw := (&ImagePattern{}).ParseImage(w.Request.Header.Get("Contatto-Raw-Image"))
rawStr := w.Request.Header.Get("Contatto-Raw-Image")
mirrorStr := w.Request.Header.Get("Contatto-Mirror-Image")
if rawStr == "" || mirrorStr == "" {
slog.Debug("missing image header", "url", w.Request.URL.String())
return nil
}

raw := (&ImagePattern{}).ParseImage(rawStr)
raw.Alias = config.Registry[raw.Registry].Alias
mirror := (&ImagePattern{}).ParseImage(w.Request.Header.Get("Contatto-Mirror-Image"))
mirror := (&ImagePattern{}).ParseImage(mirrorStr)
mirror.Alias = config.Registry[mirror.Registry].Alias

log := slog.With("raw_reg", raw.Registry)
slog := slog.With("raw_reg", raw.Registry)
rule := config.Rule[raw.Registry]
cmdline, err := rule.RenderOnMissingCmd(map[string]any{
"Raw": raw, "Mirror": mirror, "raw": raw.String(), "mirror": mirror.String(),
})
if err != nil {
log.Error("failed to render on missing command", "err", err)
slog.Error("failed to render on missing command", "err", err)
return nil
}

if cmdline != "" {
log.Info("mirror image not exist, run on missing command", "cmd", cmdline)
slog.Info("mirror image not exist, run on missing command", "cmd", cmdline)
startTime := time.Now()
cmd := exec.Command("sh", "-c", cmdline)
out, err := cmd.CombinedOutput()
if err != nil {
log.Error("failed to run on missing command", "output", string(out), "err", err)
slog.Error("failed to run on missing command", "output", string(out), "err", err)
return nil
}
log.Info("on missing command finished", "took", time.Since(startTime))
slog.Info("on missing command finished", "took", time.Since(startTime))

c.RetryToRewriteResp(w, "on_missing", http.DefaultClient.Do)
}
Expand All @@ -182,22 +181,19 @@ func (c *ProxyCmd) Run() error {
}

func (c *ProxyCmd) RetryToRewriteResp(w *http.Response, reason string, do func(req *http.Request) (*http.Response, error)) {
log := slog.With("reason", reason)
startTime := time.Now()

req := w.Request.Clone(w.Request.Context())
req.RequestURI = ""
resp, err := do(req)
if err != nil {
log.Error("failed to retry request", "err", err, "took", time.Since(startTime))
slog.Warn("failed to retry request", "reason", reason, "err", err)
return
}

w.StatusCode = resp.StatusCode
w.Status = resp.Status
w.Body = resp.Body

log.Info("retry to rewrite response", "url", req.URL.String(), "took", time.Since(startTime))
slog.Info("rewrite response", "reason", reason, "url", req.URL.String())
}

type ImagePattern struct {
Expand Down

0 comments on commit 0d0a298

Please sign in to comment.