forgejo/modules/storage/minio_test.go
forgejo-backport-action 2941adfd11
Some checks failed
testing-integration / test-sqlite (push) Has been skipped
testing-integration / test-unit (push) Has been skipped
testing / frontend-checks (push) Has been skipped
testing / backend-checks (push) Has been skipped
testing / test-e2e (push) Has been skipped
testing / test-unit (push) Has been skipped
testing / test-pgsql (push) Has been skipped
testing / test-mysql (push) Has been skipped
testing / test-remote-cacher (redis) (push) Has been skipped
testing / test-sqlite (push) Has been skipped
testing / test-remote-cacher (garnet) (push) Has been skipped
testing / test-remote-cacher (valkey) (push) Has been skipped
testing / test-remote-cacher (redict) (push) Has been skipped
testing / security-check (push) Has been skipped
/ release (push) Has been cancelled
[v12.0/forgejo] fix: minio initialization can freeze indefinitely if misconfigured (#8914)
**Backport:** https://codeberg.org/forgejo/forgejo/pulls/8897

Fixes #8893 by using a 30 second initialization timeout on the minio storage.  Manually tested by configuring `MINIO_ENDPOINT=100.64.123.123`...

```
2025/08/14 11:29:29 ...s/storage/storage.go:157:initAttachments() [I] Initialising Attachment storage with type: minio
2025/08/14 11:29:29 ...les/storage/minio.go💯NewMinioStorage() [I] Creating Minio storage at 100.64.123.123:mfenniak-forgejo with base path attachments/
2025/08/14 11:29:59 routers/init.go:63:mustInit() [F] forgejo.org/modules/storage.Init failed: Get "http://100.64.123.123/mfenniak-forgejo/?versioning=": context deadline exceeded
```

## Checklist

The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org).

### Tests

- I added test coverage for Go changes...
  - [ ] in their respective `*_test.go` for unit tests.
  - [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server.
- I added test coverage for JavaScript changes...
  - [ ] in `web_src/js/*.test.js` if it can be unit tested.
  - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)).

### Documentation

- [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change.
- [x] I did not document these changes and I do not expect someone else to do it.

### Release notes

- [ ] I do not want this change to show in the release notes.
- [x] I want the title to show in the release notes with a link to this pull request.
- [ ] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title.

Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net>
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8914
Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org>
Co-authored-by: forgejo-backport-action <forgejo-backport-action@noreply.codeberg.org>
Co-committed-by: forgejo-backport-action <forgejo-backport-action@noreply.codeberg.org>
2025-08-16 06:43:42 +02:00

259 lines
8.3 KiB
Go

// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package storage
import (
"context"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"forgejo.org/modules/setting"
"forgejo.org/modules/test"
"github.com/minio/minio-go/v7"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMinioStorageIterator(t *testing.T) {
endpoint := os.Getenv("TEST_MINIO_ENDPOINT")
if endpoint == "" {
t.Skip("TEST_MINIO_ENDPOINT not set")
return
}
testStorageIterator(t, setting.MinioStorageType, &setting.Storage{
MinioConfig: setting.MinioStorageConfig{
Endpoint: endpoint,
AccessKeyID: "123456",
SecretAccessKey: "12345678",
Bucket: "gitea",
Location: "us-east-1",
},
})
}
func TestVirtualHostMinioStorage(t *testing.T) {
endpoint := os.Getenv("TEST_MINIO_ENDPOINT")
if endpoint == "" {
t.Skip("TEST_MINIO_ENDPOINT not set")
return
}
testStorageIterator(t, setting.MinioStorageType, &setting.Storage{
MinioConfig: setting.MinioStorageConfig{
Endpoint: endpoint,
AccessKeyID: "123456",
SecretAccessKey: "12345678",
Bucket: "gitea",
Location: "us-east-1",
BucketLookup: "dns",
},
})
}
func TestMinioStoragePath(t *testing.T) {
m := &MinioStorage{basePath: ""}
assert.Empty(t, m.buildMinioPath("/"))
assert.Empty(t, m.buildMinioPath("."))
assert.Equal(t, "a", m.buildMinioPath("/a"))
assert.Equal(t, "a/b", m.buildMinioPath("/a/b/"))
assert.Empty(t, m.buildMinioDirPrefix(""))
assert.Equal(t, "a/", m.buildMinioDirPrefix("/a/"))
m = &MinioStorage{basePath: "/"}
assert.Empty(t, m.buildMinioPath("/"))
assert.Empty(t, m.buildMinioPath("."))
assert.Equal(t, "a", m.buildMinioPath("/a"))
assert.Equal(t, "a/b", m.buildMinioPath("/a/b/"))
assert.Empty(t, m.buildMinioDirPrefix(""))
assert.Equal(t, "a/", m.buildMinioDirPrefix("/a/"))
m = &MinioStorage{basePath: "/base"}
assert.Equal(t, "base", m.buildMinioPath("/"))
assert.Equal(t, "base", m.buildMinioPath("."))
assert.Equal(t, "base/a", m.buildMinioPath("/a"))
assert.Equal(t, "base/a/b", m.buildMinioPath("/a/b/"))
assert.Equal(t, "base/", m.buildMinioDirPrefix(""))
assert.Equal(t, "base/a/", m.buildMinioDirPrefix("/a/"))
m = &MinioStorage{basePath: "/base/"}
assert.Equal(t, "base", m.buildMinioPath("/"))
assert.Equal(t, "base", m.buildMinioPath("."))
assert.Equal(t, "base/a", m.buildMinioPath("/a"))
assert.Equal(t, "base/a/b", m.buildMinioPath("/a/b/"))
assert.Equal(t, "base/", m.buildMinioDirPrefix(""))
assert.Equal(t, "base/a/", m.buildMinioDirPrefix("/a/"))
}
func TestS3StorageBadRequest(t *testing.T) {
endpoint := os.Getenv("TEST_MINIO_ENDPOINT")
if endpoint == "" {
t.Skip("TEST_MINIO_ENDPOINT not set")
return
}
cfg := &setting.Storage{
MinioConfig: setting.MinioStorageConfig{
Endpoint: endpoint,
AccessKeyID: "123456",
SecretAccessKey: "12345678",
Bucket: "bucket",
Location: "us-east-1",
},
}
message := "ERROR"
old := getBucketVersioning
defer func() { getBucketVersioning = old }()
getBucketVersioning = func(ctx context.Context, minioClient *minio.Client, bucket string) error {
return minio.ErrorResponse{
StatusCode: http.StatusBadRequest,
Code: "FixtureError",
Message: message,
}
}
_, err := NewStorage(setting.MinioStorageType, cfg)
require.ErrorContains(t, err, message)
}
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,
}
creds := buildMinioCredentials(cfg, FakeEndpoint)
v, err := creds.Get()
require.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{}
t.Run("EnvMinio", func(t *testing.T) {
t.Setenv("MINIO_ACCESS_KEY", ExpectedAccessKey+"Minio")
t.Setenv("MINIO_SECRET_KEY", ExpectedSecretAccessKey+"Minio")
creds := buildMinioCredentials(cfg, FakeEndpoint)
v, err := creds.Get()
require.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, FakeEndpoint)
v, err := creds.Get()
require.NoError(t, err)
assert.Equal(t, ExpectedAccessKey+"AWS", v.AccessKeyID)
assert.Equal(t, ExpectedSecretAccessKey+"AWS", v.SecretAccessKey)
})
t.Run("FileMinio", func(t *testing.T) {
t.Setenv("MINIO_SHARED_CREDENTIALS_FILE", "testdata/minio.json")
// prevent loading any actual credentials files from the user
t.Setenv("AWS_SHARED_CREDENTIALS_FILE", "testdata/fake")
creds := buildMinioCredentials(cfg, FakeEndpoint)
v, err := creds.Get()
require.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, FakeEndpoint)
v, err := creds.Get()
require.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(cfg, server.URL)
v, err := creds.Get()
require.NoError(t, err)
assert.Equal(t, ExpectedAccessKey+"IAM", v.AccessKeyID)
assert.Equal(t, ExpectedSecretAccessKey+"IAM", v.SecretAccessKey)
})
})
}
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)
}