Fix "oras" OCI client compatibility (#34666) (#34671)

Backport #34666 by wxiaoguang

Fix #25846

1. the ImageConfig can be empty, fall back to default
2. the blob size can be empty, it still needs "Content-Length" header

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Giteabot 2025-06-10 03:20:34 +08:00 committed by GitHub
parent bf5d00074d
commit 18dc41d6f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 35 additions and 8 deletions

View File

@ -4,6 +4,7 @@
package container package container
import ( import (
"errors"
"fmt" "fmt"
"io" "io"
"strings" "strings"
@ -83,7 +84,8 @@ func ParseImageConfig(mt string, r io.Reader) (*Metadata, error) {
func parseOCIImageConfig(r io.Reader) (*Metadata, error) { func parseOCIImageConfig(r io.Reader) (*Metadata, error) {
var image oci.Image var image oci.Image
if err := json.NewDecoder(r).Decode(&image); err != nil { // EOF means empty input, still use the default data
if err := json.NewDecoder(r).Decode(&image); err != nil && !errors.Is(err, io.EOF) {
return nil, err return nil, err
} }

View File

@ -11,6 +11,7 @@ import (
oci "github.com/opencontainers/image-spec/specs-go/v1" oci "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestParseImageConfig(t *testing.T) { func TestParseImageConfig(t *testing.T) {
@ -59,3 +60,9 @@ func TestParseImageConfig(t *testing.T) {
assert.Equal(t, projectURL, metadata.ProjectURL) assert.Equal(t, projectURL, metadata.ProjectURL)
assert.Equal(t, repositoryURL, metadata.RepositoryURL) assert.Equal(t, repositoryURL, metadata.RepositoryURL)
} }
func TestParseOCIImageConfig(t *testing.T) {
metadata, err := parseOCIImageConfig(strings.NewReader(""))
require.NoError(t, err)
assert.Equal(t, &Metadata{Type: TypeOCI, Platform: DefaultPlatform, ImageLayers: []string{}}, metadata)
}

View File

@ -21,6 +21,7 @@ import (
"code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
packages_module "code.gitea.io/gitea/modules/packages" packages_module "code.gitea.io/gitea/modules/packages"
container_module "code.gitea.io/gitea/modules/packages/container" container_module "code.gitea.io/gitea/modules/packages/container"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -50,7 +51,7 @@ type containerHeaders struct {
Range string Range string
Location string Location string
ContentType string ContentType string
ContentLength int64 ContentLength optional.Option[int64]
} }
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#legacy-docker-support-http-headers // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#legacy-docker-support-http-headers
@ -64,8 +65,8 @@ func setResponseHeaders(resp http.ResponseWriter, h *containerHeaders) {
if h.ContentType != "" { if h.ContentType != "" {
resp.Header().Set("Content-Type", h.ContentType) resp.Header().Set("Content-Type", h.ContentType)
} }
if h.ContentLength != 0 { if h.ContentLength.Has() {
resp.Header().Set("Content-Length", strconv.FormatInt(h.ContentLength, 10)) resp.Header().Set("Content-Length", strconv.FormatInt(h.ContentLength.Value(), 10))
} }
if h.UploadUUID != "" { if h.UploadUUID != "" {
resp.Header().Set("Docker-Upload-Uuid", h.UploadUUID) resp.Header().Set("Docker-Upload-Uuid", h.UploadUUID)
@ -505,7 +506,7 @@ func HeadBlob(ctx *context.Context) {
setResponseHeaders(ctx.Resp, &containerHeaders{ setResponseHeaders(ctx.Resp, &containerHeaders{
ContentDigest: blob.Properties.GetByName(container_module.PropertyDigest), ContentDigest: blob.Properties.GetByName(container_module.PropertyDigest),
ContentLength: blob.Blob.Size, ContentLength: optional.Some(blob.Blob.Size),
Status: http.StatusOK, Status: http.StatusOK,
}) })
} }
@ -644,7 +645,7 @@ func HeadManifest(ctx *context.Context) {
setResponseHeaders(ctx.Resp, &containerHeaders{ setResponseHeaders(ctx.Resp, &containerHeaders{
ContentDigest: manifest.Properties.GetByName(container_module.PropertyDigest), ContentDigest: manifest.Properties.GetByName(container_module.PropertyDigest),
ContentType: manifest.Properties.GetByName(container_module.PropertyMediaType), ContentType: manifest.Properties.GetByName(container_module.PropertyMediaType),
ContentLength: manifest.Blob.Size, ContentLength: optional.Some(manifest.Blob.Size),
Status: http.StatusOK, Status: http.StatusOK,
}) })
} }
@ -708,14 +709,14 @@ func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor)
headers := &containerHeaders{ headers := &containerHeaders{
ContentDigest: pfd.Properties.GetByName(container_module.PropertyDigest), ContentDigest: pfd.Properties.GetByName(container_module.PropertyDigest),
ContentType: pfd.Properties.GetByName(container_module.PropertyMediaType), ContentType: pfd.Properties.GetByName(container_module.PropertyMediaType),
ContentLength: pfd.Blob.Size, ContentLength: optional.Some(pfd.Blob.Size),
Status: http.StatusOK, Status: http.StatusOK,
} }
if u != nil { if u != nil {
headers.Status = http.StatusTemporaryRedirect headers.Status = http.StatusTemporaryRedirect
headers.Location = u.String() headers.Location = u.String()
headers.ContentLength = 0 // do not set Content-Length for redirect responses headers.ContentLength = optional.None[int64]() // do not set Content-Length for redirect responses
setResponseHeaders(ctx.Resp, headers) setResponseHeaders(ctx.Resp, headers)
return return
} }

View File

@ -7,6 +7,7 @@ import (
"bytes" "bytes"
"crypto/sha256" "crypto/sha256"
"encoding/base64" "encoding/base64"
"encoding/hex"
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
@ -623,6 +624,22 @@ func TestPackageContainer(t *testing.T) {
assert.Equal(t, blobContent, resp.Body.Bytes()) assert.Equal(t, blobContent, resp.Body.Bytes())
}) })
t.Run("GetBlob/Empty", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
emptyDigestBuf := sha256.Sum256(nil)
emptyDigest := "sha256:" + hex.EncodeToString(emptyDigestBuf[:])
req := NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", url, emptyDigest), strings.NewReader("")).AddTokenAuth(userToken)
MakeRequest(t, req, http.StatusCreated)
req = NewRequest(t, "HEAD", fmt.Sprintf("%s/blobs/%s", url, emptyDigest)).AddTokenAuth(userToken)
resp := MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "0", resp.Header().Get("Content-Length"))
req = NewRequest(t, "GET", fmt.Sprintf("%s/blobs/%s", url, emptyDigest)).AddTokenAuth(userToken)
resp = MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "0", resp.Header().Get("Content-Length"))
})
t.Run("GetTagList", func(t *testing.T) { t.Run("GetTagList", func(t *testing.T) {
defer tests.PrintCurrentTest(t)() defer tests.PrintCurrentTest(t)()