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:
419
internal/module/registry/manifest.go
Normal file
419
internal/module/registry/manifest.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user