feat: complete OCI registry implementation with docker push/pull support

A lightweight OCI (Open Container Initiative) registry implementation written in Go.
This commit is contained in:
loveuer
2025-11-09 22:46:27 +08:00
commit 29088a6b54
45 changed files with 5629 additions and 0 deletions

View File

@@ -0,0 +1,419 @@
package registry
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"strings"
"gitea.loveuer.com/loveuer/cluster/internal/model"
"gitea.loveuer.com/loveuer/cluster/pkg/resp"
"gitea.loveuer.com/loveuer/cluster/pkg/store"
"github.com/gofiber/fiber/v3"
"gorm.io/gorm"
)
// isDigestFormat checks if a string is in digest format (e.g., sha256:abc123...)
func isDigestFormat(s string) bool {
parts := strings.SplitN(s, ":", 2)
if len(parts) != 2 {
return false
}
algo := parts[0]
hash := parts[1]
// Check algorithm
if algo != "sha256" {
// Could be extended to support other algorithms like sha512
return false
}
// Check that hash is a valid hex string of expected length (64 for sha256)
if len(hash) != 64 {
return false
}
// Verify it's all hex characters
for _, r := range hash {
if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f')) {
return false
}
}
return true
}
// HandleManifest ?? manifest ????
// PUT /v2/{repo}/manifests/{tag} - ?? manifest
// GET /v2/{repo}/manifests/{tag} - ?? manifest
// DELETE /v2/{repo}/manifests/{tag} - ?? manifest
func HandleManifest(c fiber.Ctx, db *gorm.DB, store store.Store) error {
path := c.Path()
method := c.Method()
// ????: /v2/{repo}/manifests/{tag}
// ??????????? "test/redis"
pathWithoutV2 := strings.TrimPrefix(path, "/v2/")
parts := strings.Split(pathWithoutV2, "/")
if len(parts) < 2 {
return resp.R404(c, "INVALID_PATH", nil, "invalid path")
}
// ?? "manifests" ???
manifestsIndex := -1
for i, part := range parts {
if part == "manifests" {
manifestsIndex = i
break
}
}
if manifestsIndex < 1 || manifestsIndex >= len(parts)-1 {
return resp.R404(c, "INVALID_PATH", nil, "invalid path: manifests not found")
}
// ???? manifests ???????
repo := strings.Join(parts[:manifestsIndex], "/")
// tag ? manifests ?????
tag := parts[manifestsIndex+1]
switch method {
case "PUT":
return handleManifestPut(c, db, store, repo, tag)
case "GET":
return handleManifestGet(c, db, store, repo, tag)
case "HEAD":
return handleManifestHead(c, db, store, repo, tag)
case "DELETE":
return handleManifestDelete(c, db, store, repo, tag)
}
return resp.R404(c, "NOT_FOUND", nil, "method not allowed")
}
// handleManifestPut ?? manifest
func handleManifestPut(c fiber.Ctx, db *gorm.DB, store store.Store, repo string, tag string) error {
// ?? manifest ??
content := c.Body()
if len(content) == 0 {
return resp.R400(c, "EMPTY_BODY", nil, "manifest content is empty")
}
// ?? digest
hasher := sha256.New()
hasher.Write(content)
digest := "sha256:" + hex.EncodeToString(hasher.Sum(nil))
// ?? Content-Type
mediaType := c.Get("Content-Type")
if mediaType == "" {
// ??? manifest ????
var mf map[string]interface{}
if err := json.Unmarshal(content, &mf); err == nil {
if mt, ok := mf["mediaType"].(string); ok {
mediaType = mt
} else {
// ???? Docker manifest v2
mediaType = "application/vnd.docker.distribution.manifest.v2+json"
}
} else {
mediaType = "application/vnd.docker.distribution.manifest.v2+json"
}
}
// ?? manifest ????????
var manifestData map[string]interface{}
if err := json.Unmarshal(content, &manifestData); err != nil {
return resp.R400(c, "INVALID_MANIFEST", nil, "invalid manifest format")
}
// ??????
var repository model.Repository
if err := db.Where("name = ?", repo).First(&repository).Error; err != nil {
if err == gorm.ErrRecordNotFound {
repository = model.Repository{Name: repo}
if err := db.Create(&repository).Error; err != nil {
return resp.R500(c, "", nil, err)
}
} else {
return resp.R500(c, "", nil, err)
}
}
// ?? manifest ?????
if err := store.WriteManifest(c.Context(), digest, content); err != nil {
return resp.R500(c, "", nil, err)
}
// ?? manifest ?????
var manifest model.Manifest
if err := db.Where("digest = ?", digest).First(&manifest).Error; err != nil {
if err == gorm.ErrRecordNotFound {
// ???? manifest ??
manifest = model.Manifest{
Repository: repo,
Tag: tag,
Digest: digest,
MediaType: mediaType,
Size: int64(len(content)),
Content: content,
}
if err := db.Create(&manifest).Error; err != nil {
return resp.R500(c, "", nil, err)
}
} else {
return resp.R500(c, "", nil, err)
}
} else {
// ???? manifest ? tag ??
manifest.Tag = tag
manifest.Repository = repo
if err := db.Save(&manifest).Error; err != nil {
return resp.R500(c, "", nil, err)
}
}
// ????? tag ??
var tagRecord model.Tag
if err := db.Where("repository = ? AND tag = ?", repo, tag).First(&tagRecord).Error; err != nil {
if err == gorm.ErrRecordNotFound {
tagRecord = model.Tag{
Repository: repo,
Tag: tag,
Digest: digest,
}
if err := db.Create(&tagRecord).Error; err != nil {
return resp.R500(c, "", nil, err)
}
} else {
return resp.R500(c, "", nil, err)
}
} else {
tagRecord.Digest = digest
if err := db.Save(&tagRecord).Error; err != nil {
return resp.R500(c, "", nil, err)
}
}
// ?????
c.Set("Location", fmt.Sprintf("/v2/%s/manifests/%s", repo, tag))
c.Set("Docker-Content-Digest", digest)
c.Set("Content-Type", mediaType)
c.Set("Content-Length", fmt.Sprintf("%d", len(content)))
return c.SendStatus(201)
}
// handleManifestGet ?? manifest
func handleManifestGet(c fiber.Ctx, db *gorm.DB, store store.Store, repo string, tag string) error {
var manifest model.Manifest
// ?? tag ??????????????????????
if isDigestFormat(tag) {
// ?? digest ???????????? repository
digest := tag
// ?? manifest ???
if err := db.Where("digest = ?", digest).First(&manifest).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return resp.R404(c, "MANIFEST_NOT_FOUND", nil, "manifest not found")
}
return resp.R500(c, "", nil, err)
}
// ???? manifest ?????????? repository ??
var tagRecord model.Tag
if err := db.Where("repository = ? AND digest = ?", repo, digest).First(&tagRecord).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return resp.R404(c, "MANIFEST_NOT_FOUND", nil, "manifest not found in this repository")
}
return resp.R500(c, "", nil, err)
}
} else {
// ?? tag ???? tag ?????????
var tagRecord model.Tag
if err := db.Where("repository = ? AND tag = ?", repo, tag).First(&tagRecord).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return resp.R404(c, "MANIFEST_NOT_FOUND", nil, "manifest not found")
}
return resp.R500(c, "", nil, err)
}
// ?? manifest ??
if err := db.Where("digest = ?", tagRecord.Digest).First(&manifest).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return resp.R404(c, "MANIFEST_NOT_FOUND", nil, "manifest not found")
}
return resp.R500(c, "", nil, err)
}
}
// Check Accept header if provided
acceptHeader := c.Get("Accept")
if acceptHeader != "" {
// Parse Accept header to check if client accepts the manifest's media type
acceptTypes := strings.Split(acceptHeader, ",")
accepted := false
for _, at := range acceptTypes {
// Remove quality values (e.g., "application/vnd.docker.distribution.manifest.v2+json;q=0.9")
mediaType := strings.TrimSpace(strings.Split(at, ";")[0])
if mediaType == manifest.MediaType || mediaType == "*/*" {
accepted = true
break
}
}
if !accepted {
// Check for wildcard or common Docker manifest types
for _, at := range acceptTypes {
mediaType := strings.TrimSpace(strings.Split(at, ";")[0])
if strings.Contains(mediaType, "manifest") || mediaType == "*/*" {
accepted = true
break
}
}
}
// Note: We still return the manifest even if not explicitly accepted,
// as some clients may not send proper Accept headers
}
// Read manifest content
content, err := store.ReadManifest(c.Context(), manifest.Digest)
if err != nil {
return resp.R500(c, "", nil, err)
}
// Set response headers
c.Set("Content-Type", manifest.MediaType)
c.Set("Content-Length", fmt.Sprintf("%d", len(content)))
c.Set("Docker-Content-Digest", manifest.Digest)
return c.Send(content)
}
// handleManifestHead ?? manifest ????
func handleManifestHead(c fiber.Ctx, db *gorm.DB, store store.Store, repo string, tag string) error {
var manifest model.Manifest
// ?? tag ??????????????????????
if isDigestFormat(tag) {
// ?? digest ???????????? repository
digest := tag
// ?? manifest ???
if err := db.Where("digest = ?", digest).First(&manifest).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return resp.R404(c, "MANIFEST_NOT_FOUND", nil, "manifest not found")
}
return resp.R500(c, "", nil, err)
}
// ???? manifest ?????????? repository ??
var tagRecord model.Tag
if err := db.Where("repository = ? AND digest = ?", repo, digest).First(&tagRecord).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return resp.R404(c, "MANIFEST_NOT_FOUND", nil, "manifest not found in this repository")
}
return resp.R500(c, "", nil, err)
}
} else {
// ?? tag ???? tag ?????????
var tagRecord model.Tag
if err := db.Where("repository = ? AND tag = ?", repo, tag).First(&tagRecord).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return resp.R404(c, "MANIFEST_NOT_FOUND", nil, "manifest not found")
}
return resp.R500(c, "", nil, err)
}
// ?? manifest ??
if err := db.Where("digest = ?", tagRecord.Digest).First(&manifest).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return resp.R404(c, "MANIFEST_NOT_FOUND", nil, "manifest not found")
}
return resp.R500(c, "", nil, err)
}
}
// ?????
c.Set("Content-Type", manifest.MediaType)
c.Set("Content-Length", fmt.Sprintf("%d", manifest.Size))
c.Set("Docker-Content-Digest", manifest.Digest)
return c.SendStatus(200)
}
// handleManifestDelete ?? manifest
func handleManifestDelete(c fiber.Ctx, db *gorm.DB, store store.Store, repo string, tag string) error {
var digest string
if isDigestFormat(tag) {
// ?? digest ???????????? repository
digest = tag
// ???? manifest ?????????? repository ??
var tagRecord model.Tag
if err := db.Where("repository = ? AND digest = ?", repo, digest).First(&tagRecord).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return resp.R404(c, "MANIFEST_NOT_FOUND", nil, "manifest not found in this repository")
}
return resp.R500(c, "", nil, err)
}
// ???????? tag ??? manifest
var count int64
if err := db.Model(&model.Tag{}).Where("digest = ?", digest).Count(&count).Error; err != nil {
return resp.R500(c, "", nil, err)
}
// ??? tag ??????? manifest ??
if count == 0 {
var manifest model.Manifest
if err := db.Where("digest = ?", digest).First(&manifest).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return resp.R404(c, "MANIFEST_NOT_FOUND", nil, "manifest not found")
}
return resp.R500(c, "", nil, err)
}
if err := db.Delete(&manifest).Error; err != nil {
return resp.R500(c, "", nil, err)
}
} else {
// ?? tag ?????????????????
// ??? manifest ???????????
return resp.R400(c, "CANNOT_DELETE_DIGEST_REFERENCED_BY_TAGS", nil, "cannot delete manifest referenced by tags")
}
} else {
// ?? tag ???? tag ?????????
var tagRecord model.Tag
if err := db.Where("repository = ? AND tag = ?", repo, tag).First(&tagRecord).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return resp.R404(c, "MANIFEST_NOT_FOUND", nil, "manifest not found")
}
return resp.R500(c, "", nil, err)
}
digest = tagRecord.Digest
// ?? tag ??
if err := db.Delete(&tagRecord).Error; err != nil {
return resp.R500(c, "", nil, err)
}
// ???????? tag ??? manifest
var count int64
if err := db.Model(&model.Tag{}).Where("digest = ?", digest).Count(&count).Error; err != nil {
return resp.R500(c, "", nil, err)
}
// ?????? tag ????? manifest ??
if count == 0 {
var manifest model.Manifest
if err := db.Where("digest = ?", digest).First(&manifest).Error; err == nil {
if err := db.Delete(&manifest).Error; err != nil {
return resp.R500(c, "", nil, err)
}
}
}
}
return c.SendStatus(202)
}