* Enforce IAM for s3tables bucket creation
* Prefer IAM path when policies exist
* Ensure IAM enforcement honors default allow
* address comments
* Reused the precomputed principal when setting tableBucketMetadata.OwnerAccountID, avoiding the redundant getAccountID call.
* get identity
* fix
* dedup
* fix
* comments
* fix tests
* update iam config
* go fmt
* fix ports
* fix flags
* mini clean shutdown
* Revert "update iam config"
This reverts commit ca48fdbb0afa45657823d98657556c0bbf24f239.
Revert "mini clean shutdown"
This reverts commit 9e17f6baffd5dd7cc404d831d18dd618b9fe5049.
Revert "fix flags"
This reverts commit e9e7b29d2f77ee5cb82147d50621255410695ee3.
Revert "go fmt"
This reverts commit bd3241960b1d9484b7900190773b0ecb3f762c9a.
* test/s3tables: share single weed mini per test package via TestMain
Previously each top-level test function in the catalog and s3tables
package started and stopped its own weed mini instance. This caused
failures when a prior instance wasn't cleanly stopped before the next
one started (port conflicts, leaked global state).
Changes:
- catalog/iceberg_catalog_test.go: introduce TestMain that starts one
shared TestEnvironment (external weed binary) before all tests and
tears it down after. All individual test functions now use sharedEnv.
Added randomSuffix() for unique resource names across tests.
- catalog/pyiceberg_test.go: updated to use sharedEnv instead of
per-test environments.
- catalog/pyiceberg_test_helpers.go -> pyiceberg_test_helpers_test.go:
renamed to a _test.go file so it can access TestEnvironment which is
defined in a test file.
- table-buckets/setup.go: add package-level sharedCluster variable.
- table-buckets/s3tables_integration_test.go: introduce TestMain that
starts one shared TestCluster before all tests. TestS3TablesIntegration
now uses sharedCluster. Extract startMiniClusterInDir (no *testing.T)
for TestMain use. TestS3TablesCreateBucketIAMPolicy keeps its own
cluster (different IAM config). Remove miniClusterMutex (no longer
needed). Fix Stop() to not panic when t is nil."
* delete
* parse
* default allow should work with anonymous
* fix port
* iceberg route
The failures are from Iceberg REST using the default bucket warehouse when no prefix is provided. Your tests create random buckets, so /v1/namespaces was looking in warehouse and failing. I updated the tests to use the prefixed Iceberg routes (/v1/{bucket}/...) via a small helper.
* test(s3tables): fix port conflicts and IAM ARN matching in integration tests
- Pass -master.dir explicitly to prevent filer store directory collision
between shared cluster and per-test clusters running in the same process
- Pass -volume.port.public and -volume.publicUrl to prevent the global
publicPort flag (mutated from 0 → concrete port by first cluster) from
being reused by a second cluster, causing 'address already in use'
- Remove the flag-reset loop in Stop() that reset global flag values while
other goroutines were reading them (race → panic)
- Fix IAM policy Resource ARN in TestS3TablesCreateBucketIAMPolicy to use
wildcards (arn:aws:s3tables:*:*:bucket/<name>) because the handler
generates ARNs with its own DefaultRegion (us-east-1) and principal name
('admin'), not the test constants testRegion/testAccountID
831 lines
27 KiB
Go
831 lines
27 KiB
Go
package s3tables
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
cryptorand "crypto/rand"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"flag"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/command"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables"
|
|
)
|
|
|
|
// TestMain starts a single default weed mini cluster for the whole package and
|
|
// tears it down after all tests have completed. Tests that require a different
|
|
// cluster configuration (e.g. TestS3TablesCreateBucketIAMPolicy) start their
|
|
// own cluster independently.
|
|
func TestMain(m *testing.M) {
|
|
flag.Parse()
|
|
if testing.Short() {
|
|
// Tests self-skip with t.Skip when -short is set; no cluster needed.
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
// Create a temporary T-less context so we can use t.TempDir-equivalent.
|
|
testDir, err := os.MkdirTemp("", "seaweed-s3tables-shared-*")
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "SKIP: failed to create shared temp dir: %v\n", err)
|
|
os.Exit(0)
|
|
}
|
|
|
|
cluster, err := startMiniClusterInDir(testDir, nil)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "SKIP: failed to start shared weed mini cluster: %v\n", err)
|
|
os.RemoveAll(testDir)
|
|
os.Exit(0)
|
|
}
|
|
sharedCluster = cluster
|
|
|
|
code := m.Run()
|
|
|
|
sharedCluster.Stop()
|
|
os.RemoveAll(testDir)
|
|
os.Exit(code)
|
|
}
|
|
|
|
func TestS3TablesIntegration(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping integration test in short mode")
|
|
}
|
|
|
|
// Re-use the shared cluster started by TestMain.
|
|
client := NewS3TablesClient(sharedCluster.s3Endpoint, testRegion, testAccessKey, testSecretKey)
|
|
|
|
// Run test suite
|
|
t.Run("TableBucketLifecycle", func(t *testing.T) {
|
|
testTableBucketLifecycle(t, client)
|
|
})
|
|
|
|
t.Run("NamespaceLifecycle", func(t *testing.T) {
|
|
testNamespaceLifecycle(t, client)
|
|
})
|
|
|
|
t.Run("TableLifecycle", func(t *testing.T) {
|
|
testTableLifecycle(t, client)
|
|
})
|
|
|
|
t.Run("TableBucketPolicy", func(t *testing.T) {
|
|
testTableBucketPolicy(t, client)
|
|
})
|
|
|
|
t.Run("TablePolicy", func(t *testing.T) {
|
|
testTablePolicy(t, client)
|
|
})
|
|
|
|
t.Run("Tagging", func(t *testing.T) {
|
|
testTagging(t, client)
|
|
})
|
|
|
|
t.Run("TargetOperations", func(t *testing.T) {
|
|
testTargetOperations(t, client)
|
|
})
|
|
}
|
|
|
|
func TestS3TablesCreateBucketIAMPolicy(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping IAM integration test in short mode")
|
|
}
|
|
|
|
t.Setenv("AWS_ACCESS_KEY_ID", "env-admin")
|
|
t.Setenv("AWS_SECRET_ACCESS_KEY", "env-secret")
|
|
|
|
allowedBucket := "tables-allowed"
|
|
deniedBucket := "tables-denied"
|
|
iamConfigDir := t.TempDir()
|
|
iamConfigPath := filepath.Join(iamConfigDir, "iam_config.json")
|
|
iamConfig := fmt.Sprintf(`{
|
|
"sts": {
|
|
"tokenDuration": "1h",
|
|
"maxSessionLength": "12h",
|
|
"issuer": "seaweedfs-sts",
|
|
"signingKey": "%s"
|
|
},
|
|
"accounts": [
|
|
{
|
|
"id": "%s",
|
|
"displayName": "tables-integration"
|
|
}
|
|
],
|
|
"identities": [
|
|
{
|
|
"name": "admin",
|
|
"credentials": [
|
|
{
|
|
"accessKey": "%s",
|
|
"secretKey": "%s"
|
|
}
|
|
],
|
|
"account": {
|
|
"id": "%s",
|
|
"displayName": "tables-integration"
|
|
},
|
|
"policyNames": ["S3TablesBucketPolicy"]
|
|
}
|
|
],
|
|
"policy": {
|
|
"defaultEffect": "Deny",
|
|
"storeType": "memory"
|
|
},
|
|
"policies": [
|
|
{
|
|
"name": "S3TablesBucketPolicy",
|
|
"document": {
|
|
"Version": "2012-10-17",
|
|
"Statement": [
|
|
{
|
|
"Effect": "Allow",
|
|
"Action": ["s3tables:CreateTableBucket"],
|
|
"Resource": [
|
|
"arn:aws:s3tables:*:*:bucket/%s",
|
|
"arn:aws:s3:::%s"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
]
|
|
}`, testIAMSigningKey, testAccountID, testAccessKey, testSecretKey, testAccountID, allowedBucket, allowedBucket)
|
|
require.NoError(t, os.WriteFile(iamConfigPath, []byte(iamConfig), 0644))
|
|
|
|
cluster, err := startMiniClusterWithExtraArgs(t, []string{
|
|
"-s3.config=" + iamConfigPath,
|
|
"-s3.iam.config=" + iamConfigPath,
|
|
})
|
|
require.NoError(t, err, "failed to start cluster with IAM config")
|
|
defer cluster.Stop()
|
|
|
|
client := NewS3TablesClient(cluster.s3Endpoint, testRegion, testAccessKey, testSecretKey)
|
|
|
|
_, err = client.CreateTableBucket(deniedBucket, nil)
|
|
require.Error(t, err, "denied bucket creation should fail")
|
|
assert.Contains(t, err.Error(), "AccessDenied")
|
|
|
|
allowedResp, err := client.CreateTableBucket(allowedBucket, nil)
|
|
require.NoError(t, err, "allowed bucket creation should succeed")
|
|
defer func() {
|
|
_ = client.DeleteTableBucket(allowedResp.ARN)
|
|
}()
|
|
}
|
|
|
|
func testTableBucketLifecycle(t *testing.T, client *S3TablesClient) {
|
|
bucketName := "test-bucket-" + randomString(8)
|
|
|
|
// Create table bucket
|
|
createResp, err := client.CreateTableBucket(bucketName, nil)
|
|
require.NoError(t, err, "Failed to create table bucket")
|
|
assert.Contains(t, createResp.ARN, bucketName)
|
|
t.Logf("Created table bucket: %s", createResp.ARN)
|
|
|
|
// Get table bucket
|
|
getResp, err := client.GetTableBucket(createResp.ARN)
|
|
require.NoError(t, err, "Failed to get table bucket")
|
|
assert.Equal(t, bucketName, getResp.Name)
|
|
t.Logf("Got table bucket: %s", getResp.Name)
|
|
|
|
// List table buckets
|
|
listResp, err := client.ListTableBuckets("", "", 0)
|
|
require.NoError(t, err, "Failed to list table buckets")
|
|
found := false
|
|
for _, b := range listResp.TableBuckets {
|
|
if b.Name == bucketName {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "Created bucket should appear in list")
|
|
t.Logf("Listed table buckets, found %d buckets", len(listResp.TableBuckets))
|
|
|
|
// Delete table bucket
|
|
err = client.DeleteTableBucket(createResp.ARN)
|
|
require.NoError(t, err, "Failed to delete table bucket")
|
|
t.Logf("Deleted table bucket: %s", bucketName)
|
|
|
|
// Verify bucket is deleted
|
|
_, err = client.GetTableBucket(createResp.ARN)
|
|
assert.Error(t, err, "Bucket should not exist after deletion")
|
|
}
|
|
|
|
func testNamespaceLifecycle(t *testing.T, client *S3TablesClient) {
|
|
bucketName := "test-ns-bucket-" + randomString(8)
|
|
namespaceName := "test_namespace"
|
|
|
|
// Create table bucket first
|
|
createBucketResp, err := client.CreateTableBucket(bucketName, nil)
|
|
require.NoError(t, err, "Failed to create table bucket")
|
|
defer client.DeleteTableBucket(createBucketResp.ARN)
|
|
|
|
bucketARN := createBucketResp.ARN
|
|
|
|
// Create namespace
|
|
createNsResp, err := client.CreateNamespace(bucketARN, []string{namespaceName})
|
|
require.NoError(t, err, "Failed to create namespace")
|
|
assert.Equal(t, []string{namespaceName}, createNsResp.Namespace)
|
|
t.Logf("Created namespace: %s", namespaceName)
|
|
|
|
// Get namespace
|
|
getNsResp, err := client.GetNamespace(bucketARN, []string{namespaceName})
|
|
require.NoError(t, err, "Failed to get namespace")
|
|
assert.Equal(t, []string{namespaceName}, getNsResp.Namespace)
|
|
t.Logf("Got namespace: %v", getNsResp.Namespace)
|
|
|
|
// List namespaces
|
|
listNsResp, err := client.ListNamespaces(bucketARN, "", "", 0)
|
|
require.NoError(t, err, "Failed to list namespaces")
|
|
found := false
|
|
for _, ns := range listNsResp.Namespaces {
|
|
if len(ns.Namespace) > 0 && ns.Namespace[0] == namespaceName {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "Created namespace should appear in list")
|
|
t.Logf("Listed namespaces, found %d namespaces", len(listNsResp.Namespaces))
|
|
|
|
// Delete namespace
|
|
err = client.DeleteNamespace(bucketARN, []string{namespaceName})
|
|
require.NoError(t, err, "Failed to delete namespace")
|
|
t.Logf("Deleted namespace: %s", namespaceName)
|
|
|
|
// Verify namespace is deleted
|
|
_, err = client.GetNamespace(bucketARN, []string{namespaceName})
|
|
assert.Error(t, err, "Namespace should not exist after deletion")
|
|
}
|
|
|
|
func testTableLifecycle(t *testing.T, client *S3TablesClient) {
|
|
bucketName := "test-table-bucket-" + randomString(8)
|
|
namespaceName := "test_ns"
|
|
tableName := "test_table"
|
|
|
|
// Create table bucket
|
|
createBucketResp, err := client.CreateTableBucket(bucketName, nil)
|
|
require.NoError(t, err, "Failed to create table bucket")
|
|
defer client.DeleteTableBucket(createBucketResp.ARN)
|
|
|
|
bucketARN := createBucketResp.ARN
|
|
|
|
// Create namespace
|
|
_, err = client.CreateNamespace(bucketARN, []string{namespaceName})
|
|
require.NoError(t, err, "Failed to create namespace")
|
|
defer client.DeleteNamespace(bucketARN, []string{namespaceName})
|
|
|
|
// Create table with Iceberg schema
|
|
icebergMetadata := &s3tables.TableMetadata{
|
|
Iceberg: &s3tables.IcebergMetadata{
|
|
Schema: s3tables.IcebergSchema{
|
|
Fields: []s3tables.IcebergSchemaField{
|
|
{Name: "id", Type: "int", Required: true},
|
|
{Name: "name", Type: "string"},
|
|
{Name: "value", Type: "int"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
createTableResp, err := client.CreateTable(bucketARN, []string{namespaceName}, tableName, "ICEBERG", icebergMetadata, nil)
|
|
require.NoError(t, err, "Failed to create table")
|
|
assert.NotEmpty(t, createTableResp.TableARN)
|
|
assert.NotEmpty(t, createTableResp.VersionToken)
|
|
t.Logf("Created table: %s (version: %s)", createTableResp.TableARN, createTableResp.VersionToken)
|
|
|
|
// Get table
|
|
getTableResp, err := client.GetTable(bucketARN, []string{namespaceName}, tableName)
|
|
require.NoError(t, err, "Failed to get table")
|
|
assert.Equal(t, tableName, getTableResp.Name)
|
|
assert.Equal(t, "ICEBERG", getTableResp.Format)
|
|
t.Logf("Got table: %s (format: %s)", getTableResp.Name, getTableResp.Format)
|
|
|
|
// List tables
|
|
listTablesResp, err := client.ListTables(bucketARN, []string{namespaceName}, "", "", 0)
|
|
require.NoError(t, err, "Failed to list tables")
|
|
found := false
|
|
for _, tbl := range listTablesResp.Tables {
|
|
if tbl.Name == tableName {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "Created table should appear in list")
|
|
t.Logf("Listed tables, found %d tables", len(listTablesResp.Tables))
|
|
|
|
// Delete table
|
|
err = client.DeleteTable(bucketARN, []string{namespaceName}, tableName)
|
|
require.NoError(t, err, "Failed to delete table")
|
|
t.Logf("Deleted table: %s", tableName)
|
|
|
|
// Verify table is deleted
|
|
_, err = client.GetTable(bucketARN, []string{namespaceName}, tableName)
|
|
assert.Error(t, err, "Table should not exist after deletion")
|
|
}
|
|
|
|
func testTableBucketPolicy(t *testing.T, client *S3TablesClient) {
|
|
bucketName := "test-policy-bucket-" + randomString(8)
|
|
|
|
// Create table bucket
|
|
createBucketResp, err := client.CreateTableBucket(bucketName, nil)
|
|
require.NoError(t, err, "Failed to create table bucket")
|
|
defer client.DeleteTableBucket(createBucketResp.ARN)
|
|
|
|
bucketARN := createBucketResp.ARN
|
|
|
|
// Put bucket policy
|
|
policy := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":"*","Action":"s3tables:*","Resource":"*"}]}`
|
|
err = client.PutTableBucketPolicy(bucketARN, policy)
|
|
require.NoError(t, err, "Failed to put table bucket policy")
|
|
t.Logf("Put table bucket policy")
|
|
|
|
// Get bucket policy
|
|
getPolicyResp, err := client.GetTableBucketPolicy(bucketARN)
|
|
require.NoError(t, err, "Failed to get table bucket policy")
|
|
assert.Equal(t, policy, getPolicyResp.ResourcePolicy)
|
|
t.Logf("Got table bucket policy")
|
|
|
|
// Delete bucket policy
|
|
err = client.DeleteTableBucketPolicy(bucketARN)
|
|
require.NoError(t, err, "Failed to delete table bucket policy")
|
|
t.Logf("Deleted table bucket policy")
|
|
|
|
// Verify policy is deleted
|
|
_, err = client.GetTableBucketPolicy(bucketARN)
|
|
assert.Error(t, err, "Policy should not exist after deletion")
|
|
}
|
|
|
|
func testTablePolicy(t *testing.T, client *S3TablesClient) {
|
|
bucketName := "test-table-policy-bucket-" + randomString(8)
|
|
namespaceName := "test_ns"
|
|
tableName := "test_table"
|
|
|
|
// Create table bucket
|
|
createBucketResp, err := client.CreateTableBucket(bucketName, nil)
|
|
require.NoError(t, err, "Failed to create table bucket")
|
|
defer client.DeleteTableBucket(createBucketResp.ARN)
|
|
|
|
bucketARN := createBucketResp.ARN
|
|
|
|
// Create namespace
|
|
_, err = client.CreateNamespace(bucketARN, []string{namespaceName})
|
|
require.NoError(t, err, "Failed to create namespace")
|
|
defer client.DeleteNamespace(bucketARN, []string{namespaceName})
|
|
|
|
// Create table
|
|
icebergMetadata := &s3tables.TableMetadata{
|
|
Iceberg: &s3tables.IcebergMetadata{
|
|
Schema: s3tables.IcebergSchema{
|
|
Fields: []s3tables.IcebergSchemaField{
|
|
{Name: "id", Type: "int", Required: true},
|
|
{Name: "name", Type: "string"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
createTableResp, err := client.CreateTable(bucketARN, []string{namespaceName}, tableName, "ICEBERG", icebergMetadata, nil)
|
|
require.NoError(t, err, "Failed to create table")
|
|
defer client.DeleteTable(bucketARN, []string{namespaceName}, tableName)
|
|
|
|
t.Logf("Created table: %s", createTableResp.TableARN)
|
|
|
|
// Verify no policy exists initially
|
|
_, err = client.GetTablePolicy(bucketARN, []string{namespaceName}, tableName)
|
|
assert.Error(t, err, "Policy should not exist initially")
|
|
t.Logf("Verified no policy exists initially")
|
|
|
|
// Put table policy
|
|
policy := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":"*","Action":"s3tables:*","Resource":"*"}]}`
|
|
err = client.PutTablePolicy(bucketARN, []string{namespaceName}, tableName, policy)
|
|
require.NoError(t, err, "Failed to put table policy")
|
|
t.Logf("Put table policy")
|
|
|
|
// Get table policy
|
|
getPolicyResp, err := client.GetTablePolicy(bucketARN, []string{namespaceName}, tableName)
|
|
require.NoError(t, err, "Failed to get table policy")
|
|
assert.Equal(t, policy, getPolicyResp.ResourcePolicy)
|
|
t.Logf("Got table policy")
|
|
|
|
// Delete table policy
|
|
err = client.DeleteTablePolicy(bucketARN, []string{namespaceName}, tableName)
|
|
require.NoError(t, err, "Failed to delete table policy")
|
|
t.Logf("Deleted table policy")
|
|
|
|
// Verify policy is deleted
|
|
_, err = client.GetTablePolicy(bucketARN, []string{namespaceName}, tableName)
|
|
assert.Error(t, err, "Policy should not exist after deletion")
|
|
t.Logf("Verified policy deletion")
|
|
}
|
|
|
|
func testTagging(t *testing.T, client *S3TablesClient) {
|
|
bucketName := "test-tag-bucket-" + randomString(8)
|
|
|
|
// Create table bucket with tags
|
|
initialTags := map[string]string{"Environment": "test"}
|
|
createBucketResp, err := client.CreateTableBucket(bucketName, initialTags)
|
|
require.NoError(t, err, "Failed to create table bucket")
|
|
defer client.DeleteTableBucket(createBucketResp.ARN)
|
|
|
|
bucketARN := createBucketResp.ARN
|
|
|
|
// List tags
|
|
listTagsResp, err := client.ListTagsForResource(bucketARN)
|
|
require.NoError(t, err, "Failed to list tags")
|
|
assert.Equal(t, "test", listTagsResp.Tags["Environment"])
|
|
t.Logf("Listed tags: %v", listTagsResp.Tags)
|
|
|
|
// Add more tags
|
|
newTags := map[string]string{"Department": "Engineering"}
|
|
err = client.TagResource(bucketARN, newTags)
|
|
require.NoError(t, err, "Failed to tag resource")
|
|
t.Logf("Added tags")
|
|
|
|
// Verify tags
|
|
listTagsResp, err = client.ListTagsForResource(bucketARN)
|
|
require.NoError(t, err, "Failed to list tags")
|
|
assert.Equal(t, "test", listTagsResp.Tags["Environment"])
|
|
assert.Equal(t, "Engineering", listTagsResp.Tags["Department"])
|
|
t.Logf("Verified tags: %v", listTagsResp.Tags)
|
|
|
|
// Remove a tag
|
|
err = client.UntagResource(bucketARN, []string{"Environment"})
|
|
require.NoError(t, err, "Failed to untag resource")
|
|
t.Logf("Removed tag")
|
|
|
|
// Verify tag is removed
|
|
listTagsResp, err = client.ListTagsForResource(bucketARN)
|
|
require.NoError(t, err, "Failed to list tags")
|
|
_, hasEnvironment := listTagsResp.Tags["Environment"]
|
|
assert.False(t, hasEnvironment, "Environment tag should be removed")
|
|
assert.Equal(t, "Engineering", listTagsResp.Tags["Department"])
|
|
t.Logf("Verified tag removal")
|
|
}
|
|
|
|
func testTargetOperations(t *testing.T, client *S3TablesClient) {
|
|
bucketName := "test-target-bucket-" + randomString(8)
|
|
|
|
var createResp s3tables.CreateTableBucketResponse
|
|
err := client.doTargetRequestAndDecode("CreateTableBucket", &s3tables.CreateTableBucketRequest{
|
|
Name: bucketName,
|
|
}, &createResp)
|
|
require.NoError(t, err, "Failed to create table bucket via target")
|
|
defer client.doTargetRequestAndDecode("DeleteTableBucket", &s3tables.DeleteTableBucketRequest{
|
|
TableBucketARN: createResp.ARN,
|
|
}, nil)
|
|
|
|
var listResp s3tables.ListTableBucketsResponse
|
|
err = client.doTargetRequestAndDecode("ListTableBuckets", &s3tables.ListTableBucketsRequest{}, &listResp)
|
|
require.NoError(t, err, "Failed to list table buckets via target")
|
|
found := false
|
|
for _, b := range listResp.TableBuckets {
|
|
if b.Name == bucketName {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "Created bucket should appear in target list")
|
|
|
|
var getResp s3tables.GetTableBucketResponse
|
|
err = client.doTargetRequestAndDecode("GetTableBucket", &s3tables.GetTableBucketRequest{
|
|
TableBucketARN: createResp.ARN,
|
|
}, &getResp)
|
|
require.NoError(t, err, "Failed to get table bucket via target")
|
|
assert.Equal(t, bucketName, getResp.Name)
|
|
|
|
namespaceName := "target_ns"
|
|
var createNsResp s3tables.CreateNamespaceResponse
|
|
err = client.doTargetRequestAndDecode("CreateNamespace", &s3tables.CreateNamespaceRequest{
|
|
TableBucketARN: createResp.ARN,
|
|
Namespace: []string{namespaceName},
|
|
}, &createNsResp)
|
|
require.NoError(t, err, "Failed to create namespace via target")
|
|
defer client.doTargetRequestAndDecode("DeleteNamespace", &s3tables.DeleteNamespaceRequest{
|
|
TableBucketARN: createResp.ARN,
|
|
Namespace: []string{namespaceName},
|
|
}, nil)
|
|
|
|
var listNsResp s3tables.ListNamespacesResponse
|
|
err = client.doTargetRequestAndDecode("ListNamespaces", &s3tables.ListNamespacesRequest{
|
|
TableBucketARN: createResp.ARN,
|
|
}, &listNsResp)
|
|
require.NoError(t, err, "Failed to list namespaces via target")
|
|
|
|
tableName := "target_table"
|
|
var createTableResp s3tables.CreateTableResponse
|
|
err = client.doTargetRequestAndDecode("CreateTable", &s3tables.CreateTableRequest{
|
|
TableBucketARN: createResp.ARN,
|
|
Namespace: []string{namespaceName},
|
|
Name: tableName,
|
|
Format: "ICEBERG",
|
|
}, &createTableResp)
|
|
require.NoError(t, err, "Failed to create table via target")
|
|
defer client.doTargetRequestAndDecode("DeleteTable", &s3tables.DeleteTableRequest{
|
|
TableBucketARN: createResp.ARN,
|
|
Namespace: []string{namespaceName},
|
|
Name: tableName,
|
|
}, nil)
|
|
|
|
var listTablesResp s3tables.ListTablesResponse
|
|
err = client.doTargetRequestAndDecode("ListTables", &s3tables.ListTablesRequest{
|
|
TableBucketARN: createResp.ARN,
|
|
Namespace: []string{namespaceName},
|
|
}, &listTablesResp)
|
|
require.NoError(t, err, "Failed to list tables via target")
|
|
|
|
var getTableResp s3tables.GetTableResponse
|
|
err = client.doTargetRequestAndDecode("GetTable", &s3tables.GetTableRequest{
|
|
TableBucketARN: createResp.ARN,
|
|
Namespace: []string{namespaceName},
|
|
Name: tableName,
|
|
}, &getTableResp)
|
|
require.NoError(t, err, "Failed to get table via target")
|
|
assert.Equal(t, tableName, getTableResp.Name)
|
|
|
|
policy := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":"*","Action":"s3tables:*","Resource":"*"}]}`
|
|
err = client.doTargetRequestAndDecode("PutTableBucketPolicy", &s3tables.PutTableBucketPolicyRequest{
|
|
TableBucketARN: createResp.ARN,
|
|
ResourcePolicy: policy,
|
|
}, nil)
|
|
require.NoError(t, err, "Failed to put bucket policy via target")
|
|
|
|
var getPolicyResp s3tables.GetTableBucketPolicyResponse
|
|
err = client.doTargetRequestAndDecode("GetTableBucketPolicy", &s3tables.GetTableBucketPolicyRequest{
|
|
TableBucketARN: createResp.ARN,
|
|
}, &getPolicyResp)
|
|
require.NoError(t, err, "Failed to get bucket policy via target")
|
|
assert.Equal(t, policy, getPolicyResp.ResourcePolicy)
|
|
|
|
err = client.doTargetRequestAndDecode("DeleteTableBucketPolicy", &s3tables.DeleteTableBucketPolicyRequest{
|
|
TableBucketARN: createResp.ARN,
|
|
}, nil)
|
|
require.NoError(t, err, "Failed to delete bucket policy via target")
|
|
|
|
err = client.doTargetRequestAndDecode("TagResource", &s3tables.TagResourceRequest{
|
|
ResourceARN: createResp.ARN,
|
|
Tags: map[string]string{"Environment": "test"},
|
|
}, nil)
|
|
require.NoError(t, err, "Failed to tag resource via target")
|
|
|
|
var listTagsResp s3tables.ListTagsForResourceResponse
|
|
err = client.doTargetRequestAndDecode("ListTagsForResource", &s3tables.ListTagsForResourceRequest{
|
|
ResourceARN: createResp.ARN,
|
|
}, &listTagsResp)
|
|
require.NoError(t, err, "Failed to list tags via target")
|
|
assert.Equal(t, "test", listTagsResp.Tags["Environment"])
|
|
|
|
err = client.doTargetRequestAndDecode("UntagResource", &s3tables.UntagResourceRequest{
|
|
ResourceARN: createResp.ARN,
|
|
TagKeys: []string{"Environment"},
|
|
}, nil)
|
|
require.NoError(t, err, "Failed to untag resource via target")
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
// findAvailablePorts finds n available ports by binding to port 0 multiple times
|
|
// It keeps the listeners open until all ports are found to ensure uniqueness
|
|
func findAvailablePorts(n int) ([]int, error) {
|
|
listeners := make([]*net.TCPListener, n)
|
|
ports := make([]int, n)
|
|
|
|
// Open all listeners to ensure we get unique ports
|
|
for i := 0; i < n; i++ {
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
// Close valid listeners before returning error
|
|
for j := 0; j < i; j++ {
|
|
listeners[j].Close()
|
|
}
|
|
return nil, err
|
|
}
|
|
listeners[i] = listener.(*net.TCPListener)
|
|
ports[i] = listeners[i].Addr().(*net.TCPAddr).Port
|
|
}
|
|
|
|
// Close all listeners
|
|
for _, l := range listeners {
|
|
l.Close()
|
|
}
|
|
|
|
return ports, nil
|
|
}
|
|
|
|
// startMiniClusterInDir starts a weed mini instance using testDir as the data
|
|
// directory. It does not require a *testing.T so it can be called from TestMain.
|
|
// extraArgs are appended to the default mini command flags.
|
|
func startMiniClusterInDir(testDir string, extraArgs []string) (*TestCluster, error) {
|
|
// We need 10 unique ports: Master(2), Volume(2), Filer(2), S3(2), Admin(2)
|
|
ports, err := findAvailablePorts(10)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to find available ports: %v", err)
|
|
}
|
|
|
|
masterPort := ports[0]
|
|
masterGrpcPort := ports[1]
|
|
volumePort := ports[2]
|
|
volumeGrpcPort := ports[3]
|
|
filerPort := ports[4]
|
|
filerGrpcPort := ports[5]
|
|
s3Port := ports[6]
|
|
s3GrpcPort := ports[7]
|
|
adminPort := ports[8]
|
|
adminGrpcPort := ports[9]
|
|
|
|
// Ensure no configuration file from previous runs
|
|
configFile := filepath.Join(testDir, "mini.options")
|
|
_ = os.Remove(configFile)
|
|
|
|
// Create context with timeout
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
s3Endpoint := fmt.Sprintf("http://127.0.0.1:%d", s3Port)
|
|
cluster := &TestCluster{
|
|
dataDir: testDir,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
masterPort: masterPort,
|
|
volumePort: volumePort,
|
|
filerPort: filerPort,
|
|
s3Port: s3Port,
|
|
s3Endpoint: s3Endpoint,
|
|
}
|
|
|
|
// Create empty security.toml to disable JWT authentication in tests
|
|
securityToml := filepath.Join(testDir, "security.toml")
|
|
if err = os.WriteFile(securityToml, []byte("# Empty security config for testing\n"), 0644); err != nil {
|
|
cancel()
|
|
return nil, fmt.Errorf("failed to create security.toml: %v", err)
|
|
}
|
|
|
|
// Ensure AWS credentials are set (don't use t.Setenv here — we are in TestMain).
|
|
if os.Getenv("AWS_ACCESS_KEY_ID") == "" {
|
|
os.Setenv("AWS_ACCESS_KEY_ID", "admin")
|
|
}
|
|
if os.Getenv("AWS_SECRET_ACCESS_KEY") == "" {
|
|
os.Setenv("AWS_SECRET_ACCESS_KEY", "admin")
|
|
}
|
|
|
|
// Start weed mini in a goroutine by calling the command directly
|
|
cluster.wg.Add(1)
|
|
go func() {
|
|
defer cluster.wg.Done()
|
|
|
|
// Save current directory and args, restore on exit.
|
|
oldDir, _ := os.Getwd()
|
|
oldArgs := os.Args
|
|
defer func() {
|
|
os.Chdir(oldDir)
|
|
os.Args = oldArgs
|
|
}()
|
|
|
|
// Change to test directory so mini picks up security.toml.
|
|
os.Chdir(testDir)
|
|
|
|
baseArgs := []string{
|
|
"-dir=" + testDir,
|
|
"-master.dir=" + testDir,
|
|
"-master.port=" + strconv.Itoa(masterPort),
|
|
"-master.port.grpc=" + strconv.Itoa(masterGrpcPort),
|
|
"-volume.port=" + strconv.Itoa(volumePort),
|
|
"-volume.port.grpc=" + strconv.Itoa(volumeGrpcPort),
|
|
"-volume.port.public=" + strconv.Itoa(volumePort),
|
|
"-volume.publicUrl=127.0.0.1:" + strconv.Itoa(volumePort),
|
|
"-filer.port=" + strconv.Itoa(filerPort),
|
|
"-filer.port.grpc=" + strconv.Itoa(filerGrpcPort),
|
|
"-s3.port=" + strconv.Itoa(s3Port),
|
|
"-s3.port.grpc=" + strconv.Itoa(s3GrpcPort),
|
|
"-admin.port=" + strconv.Itoa(adminPort),
|
|
"-admin.port.grpc=" + strconv.Itoa(adminGrpcPort),
|
|
"-webdav.port=0", // Disable WebDAV
|
|
"-admin.ui=false", // Disable admin UI
|
|
"-master.volumeSizeLimitMB=32", // Small volumes for testing
|
|
"-ip=127.0.0.1",
|
|
"-master.peers=none", // Faster startup
|
|
"-s3.iam.readOnly=false", // Enable IAM write operations for tests
|
|
}
|
|
if len(extraArgs) > 0 {
|
|
baseArgs = append(baseArgs, extraArgs...)
|
|
}
|
|
os.Args = append([]string{"weed"}, baseArgs...)
|
|
|
|
// Suppress most logging during tests
|
|
glog.MaxSize = 1024 * 1024
|
|
|
|
// Find and run the mini command
|
|
for _, cmd := range command.Commands {
|
|
if cmd.Name() == "mini" && cmd.Run != nil {
|
|
cmd.Flag.Parse(os.Args[1:])
|
|
args := cmd.Flag.Args()
|
|
command.MiniClusterCtx = ctx
|
|
cmd.Run(cmd, args)
|
|
command.MiniClusterCtx = nil
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Wait for S3 service to be ready
|
|
if err = waitForS3Ready(cluster.s3Endpoint, 30*time.Second); err != nil {
|
|
cancel()
|
|
return nil, fmt.Errorf("S3 service failed to start: %v", err)
|
|
}
|
|
|
|
cluster.isRunning = true
|
|
return cluster, nil
|
|
}
|
|
|
|
// startMiniClusterWithExtraArgs starts a weed mini instance for a single test.
|
|
// It uses t.TempDir() for data isolation and t.Setenv for credential scoping.
|
|
func startMiniClusterWithExtraArgs(t *testing.T, extraArgs []string) (*TestCluster, error) {
|
|
t.Helper()
|
|
testDir := t.TempDir()
|
|
|
|
// Scope credentials to the test so they are restored after test completion.
|
|
if os.Getenv("AWS_ACCESS_KEY_ID") == "" {
|
|
t.Setenv("AWS_ACCESS_KEY_ID", "admin")
|
|
}
|
|
if os.Getenv("AWS_SECRET_ACCESS_KEY") == "" {
|
|
t.Setenv("AWS_SECRET_ACCESS_KEY", "admin")
|
|
}
|
|
|
|
cluster, err := startMiniClusterInDir(testDir, extraArgs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cluster.t = t
|
|
t.Logf("Test cluster started successfully at %s", cluster.s3Endpoint)
|
|
return cluster, nil
|
|
}
|
|
|
|
// Stop stops the test cluster
|
|
func (c *TestCluster) Stop() {
|
|
if c.cancel != nil {
|
|
c.cancel()
|
|
}
|
|
// Give services time to shut down gracefully
|
|
if c.isRunning {
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
// Wait for the mini goroutine to finish
|
|
done := make(chan struct{})
|
|
go func() {
|
|
c.wg.Wait()
|
|
close(done)
|
|
}()
|
|
timer := time.NewTimer(2 * time.Second)
|
|
defer timer.Stop()
|
|
select {
|
|
case <-done:
|
|
// Goroutine finished
|
|
case <-timer.C:
|
|
// Timeout - goroutine doesn't respond to context cancel.
|
|
// This may indicate the mini cluster didn't shut down cleanly.
|
|
if c.t != nil {
|
|
c.t.Log("Warning: Test cluster shutdown timed out after 2 seconds")
|
|
} else {
|
|
fmt.Println("Warning: Test cluster shutdown timed out after 2 seconds")
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// waitForS3Ready waits for the S3 service to be ready
|
|
func waitForS3Ready(endpoint string, timeout time.Duration) error {
|
|
client := &http.Client{Timeout: 1 * time.Second}
|
|
deadline := time.Now().Add(timeout)
|
|
|
|
for time.Now().Before(deadline) {
|
|
resp, err := client.Get(endpoint)
|
|
if err == nil {
|
|
resp.Body.Close()
|
|
// Wait a bit more to ensure service is fully ready
|
|
time.Sleep(500 * time.Millisecond)
|
|
return nil
|
|
}
|
|
time.Sleep(200 * time.Millisecond)
|
|
}
|
|
|
|
return fmt.Errorf("timeout waiting for S3 service at %s", endpoint)
|
|
}
|
|
|
|
// randomString generates a random string for unique naming
|
|
func randomString(length int) string {
|
|
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
|
|
b := make([]byte, length)
|
|
if _, err := cryptorand.Read(b); err != nil {
|
|
panic("failed to generate random string: " + err.Error())
|
|
}
|
|
for i := range b {
|
|
b[i] = charset[int(b[i])%len(charset)]
|
|
}
|
|
return string(b)
|
|
}
|