mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-08 03:09:15 +00:00
7733f1953f
## Issue Gitea fails to dump LFS (and other object-storage) files when Azure Blob Storage is configured as the storage backend. The dump reports: Failed to dump LFS objects: /file/path: copying contents: file does not exist This happens with any non-empty base path (the default for LFS storage), which is why the user could only work around it by using `--skip-*` flags. The root cause is in `AzureBlobStorage.IterateObjects()`: Azure's list API already returns each blob's name including the configured base path, but the code was building the read client by running that name through the base-path-prepending helper a second time. This doubled the base path (e.g. `gitea-lfs/gitea-lfs/aa/bb/hash`), pointing at a blob that doesn't exist. `Stat()` still succeeded because it doesn't touch the network, so the failure only surfaced when the dumper actually tried to read the object's contents. ## Solution Add `getBlobClientByFullName()`, which builds a blob client from a name that is already fully qualified, without re-applying `buildAzureBlobPath()`. `IterateObjects()` now uses it for names obtained from Azure's list API. `getBlobClient()` (used by `Open`, `Stat`, `Delete`, `ServeDirectURL`, which take relative paths) is unchanged in behavior. Also add `TestAzureBlobStorageDumpArchive`, a regression test that drives the real dump path (`IterateObjects` → `Stat` → `dump.Dumper.AddFileByReader` → `mholt/archives` zip writer) against a **non-empty** `BasePath`, and verifies the produced archive contains the object with the correct content. The existing Azure tests use an empty `BasePath` and never read object content via `IterateObjects`, which is why they didn't catch this. Fixes #35476 --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
161 lines
5.1 KiB
Go
161 lines
5.1 KiB
Go
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package storage
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"gitea.dev/modules/setting"
|
|
"gitea.dev/modules/test"
|
|
"gitea.dev/modules/util"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func prepareMinioStorageConfig(t *testing.T, basePath ...string) *setting.Storage {
|
|
return &setting.Storage{
|
|
MinioConfig: setting.MinioStorageConfig{
|
|
Endpoint: test.ExternalServiceHTTP(t, "TEST_MINIO_ENDPOINT", "minio:9000"),
|
|
AccessKeyID: "123456",
|
|
SecretAccessKey: "12345678",
|
|
Bucket: "gitea",
|
|
Location: "us-east-1",
|
|
BasePath: util.OptionalArg(basePath),
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestMinioStorage(t *testing.T) {
|
|
t.Run("NoBasePath", func(t *testing.T) {
|
|
config := prepareMinioStorageConfig(t)
|
|
objStore, err := NewStorage(setting.MinioStorageType, config)
|
|
require.NoError(t, err)
|
|
testStorageGeneral(t, objStore)
|
|
})
|
|
t.Run("WithBasePath", func(t *testing.T) {
|
|
config := prepareMinioStorageConfig(t, "test-base-path")
|
|
objStore, err := NewStorage(setting.MinioStorageType, config)
|
|
require.NoError(t, err)
|
|
testStorageGeneral(t, objStore)
|
|
})
|
|
}
|
|
|
|
func TestS3StorageBadRequest(t *testing.T) {
|
|
cfg := prepareMinioStorageConfig(t)
|
|
cfg.MinioConfig.SecretAccessKey = "invalid-secret"
|
|
_, err := NewStorage(setting.MinioStorageType, cfg)
|
|
assert.ErrorContains(t, err, "ObjectStorage.BucketExists: endpoint="+cfg.MinioConfig.Endpoint)
|
|
}
|
|
|
|
func TestMinioCredentials(t *testing.T) {
|
|
const (
|
|
ExpectedAccessKey = "ExampleAccessKeyID"
|
|
ExpectedSecretAccessKey = "ExampleSecretAccessKeyID"
|
|
// Use a FakeEndpoint for IAM credentials to avoid logging any
|
|
// potential real IAM credentials when running in EC2.
|
|
FakeEndpoint = "http://localhost"
|
|
)
|
|
|
|
t.Run("Static Credentials", func(t *testing.T) {
|
|
cfg := setting.MinioStorageConfig{
|
|
AccessKeyID: ExpectedAccessKey,
|
|
SecretAccessKey: ExpectedSecretAccessKey,
|
|
IamEndpoint: FakeEndpoint,
|
|
}
|
|
creds := buildMinioCredentials(cfg)
|
|
v, err := creds.Get()
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, ExpectedAccessKey, v.AccessKeyID)
|
|
assert.Equal(t, ExpectedSecretAccessKey, v.SecretAccessKey)
|
|
})
|
|
|
|
t.Run("Chain", func(t *testing.T) {
|
|
cfg := setting.MinioStorageConfig{
|
|
IamEndpoint: FakeEndpoint,
|
|
}
|
|
|
|
t.Run("EnvMinio", func(t *testing.T) {
|
|
t.Setenv("MINIO_ACCESS_KEY", ExpectedAccessKey+"Minio")
|
|
t.Setenv("MINIO_SECRET_KEY", ExpectedSecretAccessKey+"Minio")
|
|
|
|
creds := buildMinioCredentials(cfg)
|
|
v, err := creds.Get()
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, ExpectedAccessKey+"Minio", v.AccessKeyID)
|
|
assert.Equal(t, ExpectedSecretAccessKey+"Minio", v.SecretAccessKey)
|
|
})
|
|
|
|
t.Run("EnvAWS", func(t *testing.T) {
|
|
t.Setenv("AWS_ACCESS_KEY", ExpectedAccessKey+"AWS")
|
|
t.Setenv("AWS_SECRET_KEY", ExpectedSecretAccessKey+"AWS")
|
|
|
|
creds := buildMinioCredentials(cfg)
|
|
v, err := creds.Get()
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, ExpectedAccessKey+"AWS", v.AccessKeyID)
|
|
assert.Equal(t, ExpectedSecretAccessKey+"AWS", v.SecretAccessKey)
|
|
})
|
|
|
|
t.Run("FileMinio", func(t *testing.T) {
|
|
// prevent loading any actual credentials files from the user
|
|
t.Setenv("MINIO_SHARED_CREDENTIALS_FILE", "testdata/minio.json")
|
|
t.Setenv("AWS_SHARED_CREDENTIALS_FILE", "testdata/fake")
|
|
|
|
creds := buildMinioCredentials(cfg)
|
|
v, err := creds.Get()
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, ExpectedAccessKey+"MinioFile", v.AccessKeyID)
|
|
assert.Equal(t, ExpectedSecretAccessKey+"MinioFile", v.SecretAccessKey)
|
|
})
|
|
|
|
t.Run("FileAWS", func(t *testing.T) {
|
|
// prevent loading any actual credentials files from the user
|
|
t.Setenv("MINIO_SHARED_CREDENTIALS_FILE", "testdata/fake.json")
|
|
t.Setenv("AWS_SHARED_CREDENTIALS_FILE", "testdata/aws_credentials")
|
|
|
|
creds := buildMinioCredentials(cfg)
|
|
v, err := creds.Get()
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, ExpectedAccessKey+"AWSFile", v.AccessKeyID)
|
|
assert.Equal(t, ExpectedSecretAccessKey+"AWSFile", v.SecretAccessKey)
|
|
})
|
|
|
|
t.Run("IAM", func(t *testing.T) {
|
|
// prevent loading any actual credentials files from the user
|
|
t.Setenv("MINIO_SHARED_CREDENTIALS_FILE", "testdata/fake.json")
|
|
t.Setenv("AWS_SHARED_CREDENTIALS_FILE", "testdata/fake")
|
|
|
|
// Spawn a server to emulate the EC2 Instance Metadata
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// The client will actually make 3 requests here,
|
|
// first will be to get the IMDSv2 token, second to
|
|
// get the role, and third for the actual
|
|
// credentials. However, we can return credentials
|
|
// every request since we're not emulating a full
|
|
// IMDSv2 flow.
|
|
w.Write([]byte(`{"Code":"Success","AccessKeyId":"ExampleAccessKeyIDIAM","SecretAccessKey":"ExampleSecretAccessKeyIDIAM"}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
// Use the provided EC2 Instance Metadata server
|
|
creds := buildMinioCredentials(setting.MinioStorageConfig{
|
|
IamEndpoint: server.URL,
|
|
})
|
|
v, err := creds.Get()
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, ExpectedAccessKey+"IAM", v.AccessKeyID)
|
|
assert.Equal(t, ExpectedSecretAccessKey+"IAM", v.SecretAccessKey)
|
|
})
|
|
})
|
|
}
|