fix: minio initialization can freeze indefinitely if misconfigured (#8897)
Some checks are pending
/ release (push) Waiting to run
testing-integration / test-sqlite (push) Has been skipped
testing-integration / test-mariadb (v10.6) (push) Has been skipped
testing-integration / test-unit (push) Has been skipped
testing-integration / test-mariadb (v11.8) (push) Has been skipped
testing / backend-checks (push) Has been skipped
testing / frontend-checks (push) Has been skipped
testing / test-unit (push) Has been skipped
testing / test-e2e (push) Has been skipped
testing / test-mysql (push) Has been skipped
testing / test-pgsql (push) Has been skipped
testing / test-sqlite (push) Has been skipped
testing / test-remote-cacher (redis) (push) Has been skipped
testing / test-remote-cacher (valkey) (push) Has been skipped
testing / test-remote-cacher (garnet) (push) Has been skipped
testing / test-remote-cacher (redict) (push) Has been skipped
testing / security-check (push) Has been skipped

Fixes #8893 by using a 30 second initialization timeout on the minio storage.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8897
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net>
Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
This commit is contained in:
Mathieu Fenniak 2025-08-15 21:03:51 +02:00 committed by Gusted
commit f9a6657248
2 changed files with 48 additions and 3 deletions

View file

@ -76,8 +76,13 @@ var getBucketVersioning = func(ctx context.Context, minioClient *minio.Client, b
return err return err
} }
var initializationTimeout = 30 * time.Second
// NewMinioStorage returns a minio storage // NewMinioStorage returns a minio storage
func NewMinioStorage(ctx context.Context, cfg *setting.Storage) (ObjectStorage, error) { func NewMinioStorage(ctx context.Context, cfg *setting.Storage) (ObjectStorage, error) {
initCtx, cancel := context.WithTimeout(ctx, initializationTimeout)
defer cancel()
config := cfg.MinioConfig config := cfg.MinioConfig
if config.ChecksumAlgorithm != "" && config.ChecksumAlgorithm != "default" && config.ChecksumAlgorithm != "md5" { if config.ChecksumAlgorithm != "" && config.ChecksumAlgorithm != "default" && config.ChecksumAlgorithm != "md5" {
return nil, fmt.Errorf("invalid minio checksum algorithm: %s", config.ChecksumAlgorithm) return nil, fmt.Errorf("invalid minio checksum algorithm: %s", config.ChecksumAlgorithm)
@ -112,7 +117,7 @@ func NewMinioStorage(ctx context.Context, cfg *setting.Storage) (ObjectStorage,
// Otherwise even if the request itself fails (403, 404, etc), the code should still continue because the parameters seem "good" enough. // Otherwise even if the request itself fails (403, 404, etc), the code should still continue because the parameters seem "good" enough.
// Keep in mind that GetBucketVersioning requires "owner" to really succeed, so it can't be used to check the existence. // Keep in mind that GetBucketVersioning requires "owner" to really succeed, so it can't be used to check the existence.
// Not using "BucketExists (HeadBucket)" because it doesn't include detailed failure reasons. // Not using "BucketExists (HeadBucket)" because it doesn't include detailed failure reasons.
err = getBucketVersioning(ctx, minioClient, config.Bucket) err = getBucketVersioning(initCtx, minioClient, config.Bucket)
if err != nil { if err != nil {
errResp, ok := err.(minio.ErrorResponse) errResp, ok := err.(minio.ErrorResponse)
if !ok { if !ok {
@ -125,13 +130,13 @@ func NewMinioStorage(ctx context.Context, cfg *setting.Storage) (ObjectStorage,
} }
// Check to see if we already own this bucket // Check to see if we already own this bucket
exists, err := minioClient.BucketExists(ctx, config.Bucket) exists, err := minioClient.BucketExists(initCtx, config.Bucket)
if err != nil { if err != nil {
return nil, convertMinioErr(err) return nil, convertMinioErr(err)
} }
if !exists { if !exists {
if err := minioClient.MakeBucket(ctx, config.Bucket, minio.MakeBucketOptions{ if err := minioClient.MakeBucket(initCtx, config.Bucket, minio.MakeBucketOptions{
Region: config.Location, Region: config.Location,
}); err != nil { }); err != nil {
return nil, convertMinioErr(err) return nil, convertMinioErr(err)

View file

@ -9,8 +9,10 @@ import (
"net/http/httptest" "net/http/httptest"
"os" "os"
"testing" "testing"
"time"
"forgejo.org/modules/setting" "forgejo.org/modules/setting"
"forgejo.org/modules/test"
"github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -217,3 +219,41 @@ func TestMinioCredentials(t *testing.T) {
}) })
}) })
} }
func TestNewMinioStorageInitializationTimeout(t *testing.T) {
defer test.MockVariableValue(&getBucketVersioning, func(ctx context.Context, minioClient *minio.Client, bucket string) error {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(1 * time.Millisecond):
return minio.ErrorResponse{
StatusCode: http.StatusBadRequest,
Code: "TestError",
Message: "Mocked error for testing",
}
}
})()
settings := &setting.Storage{
MinioConfig: setting.MinioStorageConfig{
Endpoint: "localhost",
AccessKeyID: "123456",
SecretAccessKey: "12345678",
Bucket: "bucket",
Location: "us-east-1",
},
}
// Verify that we reach `getBucketVersioning` and return the error from our mock.
storage, err := NewMinioStorage(t.Context(), settings)
require.ErrorContains(t, err, "Mocked error for testing")
assert.Nil(t, storage)
defer test.MockVariableValue(&initializationTimeout, 1*time.Nanosecond)()
// Now that the timeout is super low, verify that we get a context deadline exceeded error from our mock.
storage, err = NewMinioStorage(t.Context(), settings)
require.Error(t, err)
require.ErrorIs(t, err, context.DeadlineExceeded, "err must be a context deadline exceeded error, but was %v", err)
assert.Nil(t, storage)
}