使用 docker API 规范 V2,我尝试通过 HTTP 实现一个简单的 Docker 注册表。每当我运行
docker push 127.0.0.1:5000/debian
时,我都会立即收到以下错误。
Using default tag: latest
The push refers to repository [127.0.0.1:5000/debian]
Get "http://127.0.0.1:5000/v2/": dial tcp 127.0.0.1:5000: connect: connection refused
奇怪的是,如果我向 URL 发出卷曲请求,它工作正常(网络浏览器也是如此)
我还尝试通过 Docker 镜像运行本地注册表
docker run -d -p 5000:5000 --name registry registry:2.7
通过这种方式推送成功,因此 Docker 推送到不安全的注册表似乎不存在根本问题。
这是我用来通过 HTTP 运行简单注册表的代码。
package main
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/labstack/echo/v4"
)
var LayerPath string
func init() {
LayerPath = GetTemporaryDirectory()
fmt.Printf("Saving artifacts to %s\n", LayerPath)
}
func GetTemporaryDirectory() string {
tempFolder, err := ioutil.TempDir("", "docker_registry")
if err != nil {
panic(err)
}
return tempFolder
}
func main() {
e := echo.New()
e.GET("/v2/*", GetFallback)
e.PUT("/v2/*", PutFallback)
e.POST("/v2/*", PostFallback)
e.PATCH("/v2/*", PatchFallback)
e.DELETE("/v2/*", DeleteFallback)
e.GET("/v2", Root)
e.HEAD("/v2/:name/blobs/:digest", Exists)
e.GET("/v2/:name/blobs/:digest", GetLayer)
e.POST("/v2/:name/blobs/uploads", StartUpload)
e.PATCH("/v2/:name/blobs/uploads/:uuid", Upload)
e.Start("127.0.0.1:5000")
}
func GetFallback(c echo.Context) error {
return echo.NewHTTPError(http.StatusNotFound)
}
func PutFallback(c echo.Context) error {
return echo.NewHTTPError(http.StatusNotFound)
}
func PostFallback(c echo.Context) error {
return echo.NewHTTPError(http.StatusNotFound)
}
func PatchFallback(c echo.Context) error {
return echo.NewHTTPError(http.StatusNotFound)
}
func DeleteFallback(c echo.Context) error {
return echo.NewHTTPError(http.StatusNotFound)
}
func Root(c echo.Context) error {
return c.String(http.StatusOK, "OK")
}
func Exists(c echo.Context) error {
//name := c.Param("name")
digest := c.Param("digest")
hash := strings.Split(digest, ":")[1]
if _, err := os.Stat(filepath.Join(LayerPath, hash)); err == nil {
fileInfo, _ := os.Stat(filepath.Join(LayerPath, hash))
c.Response().Header().Set("content-length", fmt.Sprintf("%d", fileInfo.Size()))
c.Response().Header().Set("docker-content-digest", digest)
return c.String(http.StatusOK, "OK")
}
return echo.NewHTTPError(http.StatusNotFound)
}
func GetLayer(c echo.Context) error {
//name := c.Param("name")
digest := c.Param("digest")
hash := strings.Split(digest, ":")[1]
path := filepath.Join(LayerPath, hash)
if _, err := os.Stat(path); err == nil {
fileInfo, _ := os.Stat(path)
c.Response().Header().Set("content-length", fmt.Sprintf("%d", fileInfo.Size()))
file, err := os.Open(path)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
defer file.Close()
return c.Stream(http.StatusOK, "application/octet-stream", file)
}
return echo.NewHTTPError(http.StatusNotFound)
}
func StartUpload(c echo.Context) error {
name := c.Param("name")
guid := generateUUID()
c.Response().Header().Set("location", "/v2/"+name+"/blobs/uploads/"+guid)
c.Response().Header().Set("range", "0-0")
c.Response().Header().Set("content-length", "0")
c.Response().Header().Set("docker-upload-uuid", guid)
return c.NoContent(http.StatusAccepted)
}
func Upload(c echo.Context) error {
name := c.Param("name")
uuid := c.Param("uuid")
start := c.Request().Header.Get("content-range")
if start == "" {
start = "0"
}
startPos, _ := strconv.ParseInt(strings.Split(start, "-")[0], 10, 64)
file, err := os.OpenFile(filepath.Join(LayerPath, uuid), os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
defer file.Close()
file.Seek(startPos, io.SeekStart)
if _, err := io.Copy(file, c.Request().Body); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
fileInfo, _ := file.Stat()
c.Response().Header().Set("range", fmt.Sprintf("0-%d", fileInfo.Size()-1))
c.Response().Header().Set("docker-upload-uuid", uuid)
c.Response().Header().Set("location", "/v2/"+name+"/blobs/uploads/"+uuid)
c.Response().Header().Set("content-length", "0")
return c.NoContent(http.StatusNoContent)
}
func generateUUID() string {
hash := sha256.New()
hash.Write([]byte(fmt.Sprintf("%d", time.Now().UnixNano())))
return hex.EncodeToString(hash.Sum(nil))
}
运行时
docker push
任何环回地址(localhost、127.0.01 等)均指的是 guest 操作系统(在本例中为 Lima VM,因为我正在运行 Rancher Desktop)。根据 Lima 文档,主机 IP 始终为 192.168.5.2
...并且使用该 IP 确实可以成功推送图像。