Files
Sisyphus/apps/sisyphusd/s3_test.go
T
rvnminers-A-and-N 6c3b2ab2a0 feat: a coordinator can keep its stored data in an S3 bucket (#114)
A third place for a pool's stored data, beside the coordinator's disk
and Kubo: a bucket in any object store that speaks S3. Each block is an
object; pins stay on the node, and the credentials never leave it.
2026-10-09 03:21:47 -04:00

157 lines
6.4 KiB
Go

package main
import (
"bytes"
"context"
"errors"
"fmt"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/sisyphus-network/Sisyphus/packages/s3"
)
// objectStore is a real object store, SeaweedFS with its S3 gateway in a
// container, started the first time a test wants it.
var objectStore struct {
once sync.Once
endpoint string
err error
}
const (
objectStoreAccess = "sisyphus-test"
objectStoreSecret = "sisyphus-test-secret"
)
// bucketFor starts the object store if need be, makes a bucket in it for a
// test, and returns where it is and the bucket's name. A machine with no
// Docker skips the test, unless told that it must have it.
func bucketFor(t *testing.T) (endpoint, bucket string) {
t.Helper()
objectStore.once.Do(func() {
lis, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
objectStore.err = err
return
}
port := lis.Addr().(*net.TCPAddr).Port
lis.Close()
name := fmt.Sprintf("sisyphus-daemon-s3-test-%d", os.Getpid())
out, err := exec.Command("docker", "run", "-d", "--rm", "--name", name, "-p", fmt.Sprintf("127.0.0.1:%d:8333", port),
"-e", "AWS_ACCESS_KEY_ID="+objectStoreAccess, "-e", "AWS_SECRET_ACCESS_KEY="+objectStoreSecret,
"chrislusf/seaweedfs:latest", "server", "-s3", "-dir=/data").CombinedOutput()
if err != nil {
objectStore.err = fmt.Errorf("start the object store: %v: %s", err, bytes.TrimSpace(out))
return
}
stopAtExit = append(stopAtExit, func() { exec.Command("docker", "rm", "-f", name).Run() })
at := fmt.Sprintf("http://127.0.0.1:%d", port)
for deadline := time.Now().Add(90 * time.Second); time.Now().Before(deadline); time.Sleep(250 * time.Millisecond) {
if res, err := http.Get(at); err == nil {
res.Body.Close()
objectStore.endpoint = at
return
}
}
objectStore.err = errors.New("the object store did not start in time")
})
if objectStore.err != nil {
if os.Getenv("SISYPHUS_REQUIRE_DOCKER") != "" {
t.Fatalf("no object store to test against, and SISYPHUS_REQUIRE_DOCKER is set: %v", objectStore.err)
}
t.Skipf("no object store to test against: %v", objectStore.err)
}
api, err := minio.New(strings.TrimPrefix(objectStore.endpoint, "http://"), &minio.Options{Creds: credentials.NewStaticV4(objectStoreAccess, objectStoreSecret, "")})
if err != nil {
t.Fatal(err)
}
bucket = fmt.Sprintf("test-%d", time.Now().UnixNano())
// The gateway answers before it is ready to make buckets.
for deadline := time.Now().Add(60 * time.Second); ; time.Sleep(500 * time.Millisecond) {
if err = api.MakeBucket(context.Background(), bucket, minio.MakeBucketOptions{}); err == nil {
return objectStore.endpoint, bucket
}
if time.Now().After(deadline) {
t.Fatalf("make bucket: %v", err)
}
}
}
func TestACoordinatorKeepsItsStoredDataInABucket(t *testing.T) {
endpoint, bucket := bucketFor(t)
dataDir := t.TempDir()
keys := filepath.Join(t.TempDir(), "keys")
os.WriteFile(keys, []byte(objectStoreAccess+"\n"+objectStoreSecret+"\n"), 0o600)
addr := freeAddr(t)
args := []string{"--data-dir", dataDir, "--listen", addr, "--name", "rig", "--slots", "2",
"--s3-endpoint", endpoint, "--s3-bucket", bucket, "--s3-prefix", "pool-one", "--s3-credentials", keys}
stop := startDaemon(t, args...)
waitForOutput(t, "rig", "nodes", "--addr", addr)
input := filepath.Join(t.TempDir(), "words.txt")
os.WriteFile(input, []byte(strings.Repeat("the boulder rolls and the boulder rolls again\n", 2000)), 0o600)
cid := strings.TrimSpace(mustCLI(t, "blob", "put", "--addr", addr, input))
out := mustCLI(t, "job", "submit", "--addr", addr, "--workload", "wordcount", "--params", `{"input":"`+cid+`"}`, "--tasks", "2")
if !strings.Contains(out, `"words":16000`) {
t.Fatalf("a job over data kept in a bucket:\n%s", out)
}
// The data is in the bucket, under the prefix, and nowhere on this
// machine but for the record of what is kept.
client, err := s3.Open(context.Background(), s3.Config{Endpoint: endpoint, Bucket: bucket, Prefix: "pool-one", AccessKey: objectStoreAccess, SecretKey: objectStoreSecret})
if err != nil {
t.Fatal(err)
}
blocks := 0
client.List(context.Background(), func(name string, _ int64) bool {
if strings.HasPrefix(name, "blocks/") {
blocks++
}
return true
})
if blocks < 2 {
t.Errorf("the bucket holds %d blocks", blocks)
}
if _, err := os.Stat(filepath.Join(dataDir, "blobs")); !os.IsNotExist(err) {
t.Errorf("a store on this machine's disk beside the bucket: %v", err)
}
// It is all still there for a coordinator started again.
stop()
startDaemon(t, args...)
waitForOutput(t, "rig", "nodes", "--addr", addr)
if got := mustCLI(t, "blob", "stat", "--addr", addr, cid); !strings.Contains(got, "92000") {
t.Errorf("the file after a restart: %s", got)
}
oneLine := filepath.Join(t.TempDir(), "keys")
os.WriteFile(oneLine, []byte(objectStoreAccess+"\n"), 0o600)
inUse := []string{"--data-dir", dataDir, "--s3-endpoint", endpoint, "--s3-bucket", bucket, "--s3-prefix", "pool-one", "--s3-credentials", keys}
for _, tt := range []struct {
name string
args []string
want string
}{
{"no bucket", []string{"--data-dir", t.TempDir(), "--s3-endpoint", endpoint}, "needs --s3-bucket"},
{"with Kubo too", []string{"--data-dir", t.TempDir(), "--s3-endpoint", endpoint, "--s3-bucket", bucket, "--kubo"}, "choose one"},
{"on a node with no pool", []string{"--data-dir", t.TempDir(), "--role", "worker", "--coordinator", "127.0.0.1:1", "--s3-endpoint", endpoint, "--s3-bucket", bucket}, "is for a node that coordinates a pool"},
{"credentials that are not there", []string{"--data-dir", t.TempDir(), "--s3-endpoint", endpoint, "--s3-bucket", bucket, "--s3-credentials", filepath.Join(t.TempDir(), "absent")}, "read the bucket's credentials"},
{"credentials of one line", []string{"--data-dir", t.TempDir(), "--s3-endpoint", endpoint, "--s3-bucket", bucket, "--s3-credentials", oneLine}, "the access key on its first line and the secret key on its second"},
{"a bucket that is not there", []string{"--data-dir", t.TempDir(), "--s3-endpoint", endpoint, "--s3-bucket", "no-such-bucket-here", "--s3-credentials", keys}, "there is no bucket"},
{"a data directory in use", inUse, "in use by another process"},
} {
if _, err := cli(t, append([]string{"run", "--listen", freeAddr(t)}, tt.args...)...); err == nil || !strings.Contains(err.Error(), tt.want) {
t.Errorf("--s3-endpoint %s: %v, want %q", tt.name, err, tt.want)
}
}
}