~bigbes/ci-cacher

ref: v0.1.0 ci-cacher/internal/testutil/garage/container.go -rw-r--r-- 4.1 KiB
176b6cb9 — Eugene Blikh Bump VERSION to 0.1.0 for tag 2 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// Package garage starts a single-node Garage container for end-to-end
// tests. Garage v2.3+ exposes `server --single-node --default-bucket`
// which auto-creates the bucket + access key from env vars, so the
// usual six-step CLI bootstrap (status / layout assign / layout apply /
// bucket create / key new / bucket allow) becomes a no-op.
//
// Requires Docker on the host. Each call to Start spins up a fresh
// container with random credentials and binds host ports automatically;
// t.Cleanup tears it down.
package garage

import (
	"bytes"
	"context"
	"crypto/rand"
	"encoding/base64"
	"encoding/hex"
	"fmt"
	"testing"
	"time"

	"github.com/testcontainers/testcontainers-go"
	"github.com/testcontainers/testcontainers-go/wait"
)

// Image we pull. Pin to a known-good version so test runs are reproducible.
const Image = "dxflrs/garage:v2.3.0"

// Garage holds the connection details for the running container.
type Garage struct {
	Endpoint  string // http://host:port — feed to cacher --endpoint / config endpoint
	AccessKey string // GK… — feed to CACHER_S3_KEY_ID
	SecretKey string // hex — feed to CACHER_S3_SECRET
	Bucket    string // pre-created bucket the access key owns
	Region    string // always "garage" for this image
}

// Start brings up a Garage container and waits for the S3 API to be
// responding. Container is automatically terminated via t.Cleanup.
func Start(t *testing.T) *Garage {
	t.Helper()
	ctx := context.Background()

	g := &Garage{
		AccessKey: "GK" + randHex(16),
		SecretKey: randHex(32),
		Bucket:    "test-bucket",
		Region:    "garage",
	}

	cfg := minimalTOML(randHex(32), randBase64(32))

	req := testcontainers.ContainerRequest{
		Image:        Image,
		ExposedPorts: []string{"3900/tcp"},
		Cmd:          []string{"/garage", "server", "--single-node", "--default-bucket"},
		Env: map[string]string{
			"GARAGE_DEFAULT_ACCESS_KEY": g.AccessKey,
			"GARAGE_DEFAULT_SECRET_KEY": g.SecretKey,
			"GARAGE_DEFAULT_BUCKET":     g.Bucket,
		},
		Files: []testcontainers.ContainerFile{
			{
				Reader:            bytes.NewReader([]byte(cfg)),
				ContainerFilePath: "/etc/garage.toml",
				FileMode:          0o644,
			},
		},
		// Combine port-open + log-line wait so we don't race the S3 server
		// starting up after the RPC port is bound.
		WaitingFor: wait.ForAll(
			wait.ForListeningPort("3900/tcp").WithStartupTimeout(30*time.Second),
			wait.ForLog("S3 API server listening").WithStartupTimeout(30*time.Second),
		),
	}

	c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
		ContainerRequest: req,
		Started:          true,
	})
	if err != nil {
		t.Fatalf("garage container start: %v", err)
	}
	t.Cleanup(func() {
		if err := c.Terminate(context.Background()); err != nil {
			t.Logf("garage container terminate: %v", err)
		}
	})

	host, err := c.Host(ctx)
	if err != nil {
		t.Fatalf("container host: %v", err)
	}
	port, err := c.MappedPort(ctx, "3900/tcp")
	if err != nil {
		t.Fatalf("container port: %v", err)
	}
	g.Endpoint = fmt.Sprintf("http://%s:%s", host, port.Port())
	return g
}

// minimalTOML returns the smallest garage.toml that works for a single-
// node test instance: in-memory metadata under /tmp (lost on container
// stop, which is what we want), replication_factor=1, S3 region "garage",
// random secrets for rpc + admin so the container can't conflict with
// neighbours.
func minimalTOML(rpcSecret, adminToken string) string {
	return fmt.Sprintf(`
metadata_dir = "/tmp/meta"
data_dir = "/tmp/data"
db_engine = "sqlite"

replication_factor = 1

rpc_bind_addr     = "[::]:3901"
rpc_public_addr   = "127.0.0.1:3901"
rpc_secret        = %q

[s3_api]
s3_region    = "garage"
api_bind_addr = "[::]:3900"
root_domain   = ".s3.garage.localhost"

[s3_web]
bind_addr   = "[::]:3902"
root_domain = ".web.garage.localhost"
index       = "index.html"

[admin]
api_bind_addr = "[::]:3903"
admin_token   = %q
`, rpcSecret, adminToken)
}

func randHex(n int) string {
	b := make([]byte, n)
	_, _ = rand.Read(b)
	return hex.EncodeToString(b)
}

func randBase64(n int) string {
	b := make([]byte, n)
	_, _ = rand.Read(b)
	return base64.StdEncoding.EncodeToString(b)
}