Skip to content

Commit 5d91368

Browse files
authored
DX-422: Local S3 Provider (#1854)
Local S3 Minio Provider
1 parent eefe47d commit 5d91368

File tree

16 files changed

+566
-3
lines changed

16 files changed

+566
-3
lines changed

.github/workflows/framework-golden-tests.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ jobs:
6363
config: scalability.toml
6464
count: 1
6565
timeout: 10m
66+
- name: TestLocalS3
67+
config: local_s3.toml
68+
count: 1
69+
timeout: 10m
6670
name: ${{ matrix.test.name }}
6771
steps:
6872
- name: Checkout repo

book/src/SUMMARY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@
5252
- [Chainlink](framework/components/chainlink.md)
5353
- [Node](framework/components/chainlink/node.md)
5454
- [NodeSet](framework/components/chainlink/nodeset.md)
55+
- [Storage](framework/components/storage.md)
56+
- [S3](framework/components/storage/s3.md)
5557
- [Clients]()
5658
- [Chainlink]()
5759
- [RPC]()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Storage
2+
3+
Here we store `Storage` components: S3.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# S3
2+
3+
The framework contains local S3 provider.
4+
Currently, we support [MinIO](github.com/minio/minio).
5+
6+
## Configuration
7+
```toml
8+
[local_s3]
9+
host = "minio"
10+
port = 9000
11+
console_port = 9001
12+
access_key = "(default:random)"
13+
secret_key = "(default:random)"
14+
bucket = "test-bucket"
15+
region = "us-east-1"
16+
```
17+
18+
Example values are defaults.
19+
20+
## Usage
21+
22+
```golang
23+
package my_test
24+
25+
import (
26+
"fmt"
27+
"os"
28+
"testing"
29+
30+
"github.com/smartcontractkit/chainlink-testing-framework/framework/components/s3provider"
31+
"github.com/smartcontractkit/chainlink-testing-framework/framework"
32+
"github.com/stretchr/testify/require"
33+
)
34+
35+
type Config struct {
36+
S3Config *s3provider.Input `toml:"local_s3" validate:"required"`
37+
}
38+
39+
func TestLocalS3(t *testing.T) {
40+
in, err := framework.Load[Config](t)
41+
require.NoError(t, err)
42+
43+
output, err := NewMinioFactory().NewFrom(in)
44+
require.NoError(t, err)
45+
46+
t.log(fmt.Printf("%#v", output))
47+
}
48+
```
49+
50+
Alternatively, the component supports Options pattern and can be created from code:
51+
```golang
52+
// ...
53+
s3provider, err := NewMinioFactory().New(
54+
WithPort(port),
55+
WithConsolePort(consolePort),
56+
WithAccessKey(accessKey),
57+
WithSecretKey(secretKey),
58+
)
59+
require.NoError(t, err)
60+
61+
output := s3provider.Output()
62+
// ...
63+
```

framework/.changeset/v0.8.8.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Local S3 provider
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package s3provider
2+
3+
// Provider is the interface that wraps S3 interaction methods.
4+
type Provider interface {
5+
GetEndpoint() string
6+
GetBaseEndpoint() string
7+
GetConsoleURL() string
8+
GetConsoleBaseURL() string
9+
GetSecretKey() string
10+
GetAccessKey() string
11+
GetBucket() string
12+
GetRegion() string
13+
Output() *Output
14+
}
15+
16+
// ProviderFactory is the interface that standardizes S3 providers constructors.
17+
type ProviderFactory interface {
18+
New(...Option) (Provider, error)
19+
NewFrom(*Input) (*Output, error)
20+
}
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
package s3provider
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"math/rand/v2"
7+
"net"
8+
"strconv"
9+
10+
"dario.cat/mergo"
11+
"github.com/docker/docker/api/types/container"
12+
"github.com/docker/go-connections/nat"
13+
"github.com/minio/minio-go/v7"
14+
"github.com/minio/minio-go/v7/pkg/credentials"
15+
tc "github.com/testcontainers/testcontainers-go"
16+
tcwait "github.com/testcontainers/testcontainers-go/wait"
17+
18+
"github.com/smartcontractkit/chainlink-testing-framework/framework"
19+
)
20+
21+
const (
22+
DefaultImage = "minio/minio"
23+
DefaultName = "minio"
24+
DefaultBucket = "test-bucket"
25+
DefaultRegion = "us-east-1"
26+
DefaultHost = "minio"
27+
DefaultPort = 9000
28+
DefaultConsolePort = 9001
29+
30+
accessKeyLength = 20
31+
secretKeyLength = 40
32+
)
33+
34+
type Minio struct {
35+
Host string `toml:"host"`
36+
Port int `toml:"port"`
37+
ConsolePort int `toml:"console_port"`
38+
AccessKey string `toml:"access_key"`
39+
SecretKey string `toml:"secret_key"`
40+
Bucket string `toml:"bucket"`
41+
Region string `toml:"region"`
42+
}
43+
44+
type Input = Minio
45+
46+
type Output struct {
47+
SecretKey string `toml:"secret_key"`
48+
AccessKey string `toml:"access_key"`
49+
Bucket string `toml:"bucket"`
50+
ConsoleURL string `toml:"console_url"`
51+
ConsoleBaseURL string `toml:"console_base_url"`
52+
Endpoint string `toml:"endpoint"`
53+
BaseEndpoint string `toml:"base_endpoint"`
54+
Region string `toml:"region"`
55+
UseCache bool `toml:"use_cache"`
56+
}
57+
58+
func (m Minio) Output() *Output {
59+
return &Output{
60+
AccessKey: m.GetAccessKey(),
61+
SecretKey: m.GetSecretKey(),
62+
Bucket: m.GetBucket(),
63+
ConsoleURL: m.GetConsoleURL(),
64+
ConsoleBaseURL: m.GetConsoleBaseURL(),
65+
Endpoint: m.GetEndpoint(),
66+
BaseEndpoint: m.GetBaseEndpoint(),
67+
Region: m.GetRegion(),
68+
}
69+
}
70+
71+
func (m Minio) GetSecretKey() string {
72+
return m.SecretKey
73+
}
74+
75+
func (m Minio) GetAccessKey() string {
76+
return m.AccessKey
77+
}
78+
79+
func (m Minio) GetBucket() string {
80+
return m.Bucket
81+
}
82+
83+
func (m Minio) GetConsoleURL() string {
84+
return fmt.Sprintf("http://%s", net.JoinHostPort(m.Host, strconv.Itoa(m.ConsolePort)))
85+
}
86+
87+
func (m Minio) GetConsoleBaseURL() string {
88+
return fmt.Sprintf("http://%s", net.JoinHostPort(DefaultHost, strconv.Itoa(m.ConsolePort)))
89+
}
90+
91+
func (m Minio) GetEndpoint() string {
92+
return fmt.Sprintf("%s:%d", m.Host, m.Port)
93+
}
94+
95+
func (m Minio) GetBaseEndpoint() string {
96+
return fmt.Sprintf("%s:%d", DefaultHost, m.Port)
97+
}
98+
99+
func (m Minio) GetRegion() string {
100+
return m.Region
101+
}
102+
103+
type Option func(*Minio)
104+
105+
type MinioFactory struct{}
106+
107+
func NewMinioFactory() ProviderFactory {
108+
return MinioFactory{}
109+
}
110+
111+
func (mf MinioFactory) NewFrom(input *Input) (*Output, error) {
112+
// Fill in defaults on empty
113+
err := mergo.Merge(input, DefaultMinio())
114+
if err != nil {
115+
return nil, err
116+
}
117+
118+
provider, err := mf.run(input)
119+
if err != nil {
120+
return nil, err
121+
}
122+
return provider.Output(), nil
123+
}
124+
125+
func DefaultMinio() *Minio {
126+
return &Minio{
127+
Host: DefaultHost,
128+
Port: DefaultPort,
129+
ConsolePort: DefaultConsolePort,
130+
AccessKey: randomStr(accessKeyLength),
131+
SecretKey: randomStr(secretKeyLength),
132+
Bucket: DefaultBucket,
133+
Region: DefaultRegion,
134+
}
135+
}
136+
137+
func (mf MinioFactory) New(options ...Option) (Provider, error) {
138+
m := DefaultMinio()
139+
140+
for _, opt := range options {
141+
opt(m)
142+
}
143+
144+
return mf.run(m)
145+
}
146+
147+
func (mf MinioFactory) run(m *Minio) (Provider, error) {
148+
var err error
149+
150+
ctx := context.Background()
151+
containerName := framework.DefaultTCName(DefaultName)
152+
bindPort := fmt.Sprintf("%d/tcp", m.Port)
153+
bindConsolePort := fmt.Sprintf("%d/tcp", m.ConsolePort)
154+
networks := []string{"compose_default"}
155+
networkAliases := map[string][]string{
156+
"compose_default": {DefaultName},
157+
}
158+
159+
if len(framework.DefaultNetworkName) > 0 {
160+
// attach default ctf network if initiated
161+
networks = append(networks, framework.DefaultNetworkName)
162+
networkAliases[framework.DefaultNetworkName] = []string{
163+
containerName,
164+
DefaultName,
165+
}
166+
}
167+
168+
req := tc.ContainerRequest{
169+
Name: containerName,
170+
Image: DefaultImage,
171+
Labels: framework.DefaultTCLabels(),
172+
Networks: networks,
173+
NetworkAliases: networkAliases,
174+
ExposedPorts: []string{
175+
bindPort,
176+
bindConsolePort,
177+
},
178+
Env: map[string]string{
179+
"MINIO_ROOT_USER": m.AccessKey,
180+
"MINIO_ROOT_PASSWORD": m.SecretKey,
181+
"MINIO_BUCKET": m.Bucket,
182+
},
183+
Entrypoint: []string{
184+
"minio",
185+
"server",
186+
"/data",
187+
"--address",
188+
fmt.Sprintf(":%d", m.Port),
189+
"--console-address",
190+
fmt.Sprintf(":%d", m.ConsolePort),
191+
},
192+
HostConfigModifier: func(h *container.HostConfig) {
193+
framework.NoDNS(true, h)
194+
h.PortBindings = nat.PortMap{
195+
nat.Port(bindPort): []nat.PortBinding{
196+
{
197+
HostIP: "0.0.0.0",
198+
HostPort: strconv.Itoa(m.Port),
199+
},
200+
},
201+
nat.Port(bindConsolePort): []nat.PortBinding{
202+
{
203+
HostIP: "0.0.0.0",
204+
HostPort: strconv.Itoa(m.ConsolePort),
205+
},
206+
},
207+
}
208+
},
209+
WaitingFor: tcwait.ForAll(
210+
tcwait.ForListeningPort(nat.Port(bindPort)),
211+
tcwait.ForListeningPort(nat.Port(bindConsolePort)),
212+
),
213+
}
214+
215+
c, err := tc.GenericContainer(ctx, tc.GenericContainerRequest{
216+
ContainerRequest: req,
217+
Started: true,
218+
})
219+
if err != nil {
220+
return nil, err
221+
}
222+
223+
m.Host, err = framework.GetHost(c)
224+
if err != nil {
225+
return nil, err
226+
}
227+
228+
// Initialize minio client object.
229+
minioClient, err := minio.New(m.GetEndpoint(), &minio.Options{
230+
Creds: credentials.NewStaticV4(m.GetAccessKey(), m.GetSecretKey(), ""),
231+
Secure: false,
232+
})
233+
if err != nil {
234+
framework.L.Warn().Str("error", err.Error()).Msg("failed to create minio client")
235+
236+
return nil, err
237+
}
238+
239+
// Initialize default bucket
240+
err = minioClient.MakeBucket(ctx, m.GetBucket(), minio.MakeBucketOptions{Region: m.GetRegion()})
241+
if err != nil {
242+
framework.L.Warn().Str("error", err.Error()).Msg("failed to create minio bucket")
243+
244+
return nil, err
245+
}
246+
247+
return m, nil
248+
}
249+
250+
func WithPort(port int) Option {
251+
return func(m *Minio) {
252+
m.Port = port
253+
}
254+
}
255+
256+
func WithConsolePort(consolePort int) Option {
257+
return func(m *Minio) {
258+
m.ConsolePort = consolePort
259+
}
260+
}
261+
262+
func WithAccessKey(accessKey string) Option {
263+
return func(m *Minio) {
264+
m.AccessKey = accessKey
265+
}
266+
}
267+
268+
func WithSecretKey(secretKey string) Option {
269+
return func(m *Minio) {
270+
m.SecretKey = secretKey
271+
}
272+
}
273+
274+
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
275+
276+
func randomStr(n int) string {
277+
b := make([]byte, n)
278+
for i := range b {
279+
b[i] = letterBytes[rand.IntN(len(letterBytes))] // #nosec G404
280+
}
281+
282+
return string(b)
283+
}

0 commit comments

Comments
 (0)