Skip to content

Commit 8e126cc

Browse files
committed
Switch to AWS SDK for S3 cache access
The Minio AWS library doesn't support a number of items, such as: * S3 Express One Zone * The ability to configure AWS_STS_ENDPOINT_URL for AWS Secret Cloud This new S3 client can be toggled off via the FF_USE_LEGACY_S3_CACHE_ADAPTER feature flag. Relates to https://gitlab.com/gitlab-org/gitlab-runner/-/issues/37394 Changelog: changed
1 parent cde2f20 commit 8e126cc

File tree

11 files changed

+819
-60
lines changed

11 files changed

+819
-60
lines changed

cache/cache.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ func getAdaptorForBuild(build *common.Build, key string) Adapter {
7070
build.Runner.Cache.Type = "gcsv2"
7171
}
7272

73+
if build.Runner.Cache.Type == "s3" && !build.IsFeatureFlagOn(featureflags.UseLegacyS3CacheAdapter) {
74+
build.Runner.Cache.Type = "s3v2"
75+
}
76+
7377
adapter, err := createAdapter(build.Runner.Cache, build.GetBuildTimeout(), objectName)
7478
if err != nil {
7579
logrus.WithError(err).Error("Could not create cache adapter")

cache/s3v2/adapter.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package s3v2
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"net/url"
8+
"time"
9+
10+
"github.com/sirupsen/logrus"
11+
12+
"gitlab.com/gitlab-org/gitlab-runner/cache"
13+
"gitlab.com/gitlab-org/gitlab-runner/common"
14+
)
15+
16+
type s3Adapter struct {
17+
timeout time.Duration
18+
config *common.CacheS3Config
19+
objectName string
20+
client s3Presigner
21+
}
22+
23+
func (a *s3Adapter) GetDownloadURL(ctx context.Context) *url.URL {
24+
presignedURL, err := a.presignURL(ctx, http.MethodGet)
25+
if err != nil {
26+
logrus.WithError(err).Error("error while generating S3 pre-signed URL")
27+
28+
return nil
29+
}
30+
31+
return presignedURL
32+
}
33+
34+
func (a *s3Adapter) GetUploadURL(ctx context.Context) *url.URL {
35+
presignedURL, err := a.presignURL(ctx, http.MethodPut)
36+
if err != nil {
37+
logrus.WithError(err).Error("error while generating S3 pre-signed URL")
38+
39+
return nil
40+
}
41+
42+
return presignedURL
43+
}
44+
45+
func (a *s3Adapter) GetUploadHeaders() http.Header {
46+
return nil
47+
}
48+
49+
func (a *s3Adapter) GetGoCloudURL(_ context.Context) *url.URL {
50+
return nil
51+
}
52+
53+
func (a *s3Adapter) GetUploadEnv() map[string]string {
54+
return nil
55+
}
56+
57+
func (a *s3Adapter) presignURL(ctx context.Context, method string) (*url.URL, error) {
58+
if a.config.BucketName == "" {
59+
return nil, fmt.Errorf("config BucketName cannot be empty")
60+
}
61+
62+
if a.objectName == "" {
63+
return nil, fmt.Errorf("object name cannot be empty")
64+
}
65+
66+
return a.client.PresignURL(ctx, method, a.config.BucketName, a.objectName, a.timeout)
67+
}
68+
69+
func New(config *common.CacheConfig, timeout time.Duration, objectName string) (cache.Adapter, error) {
70+
s3Config := config.S3
71+
if s3Config == nil {
72+
return nil, fmt.Errorf("missing S3 configuration")
73+
}
74+
75+
client, err := newS3Client(s3Config)
76+
if err != nil {
77+
return nil, fmt.Errorf("error while creating S3 cache storage client: %w", err)
78+
}
79+
80+
a := &s3Adapter{
81+
config: s3Config,
82+
timeout: timeout,
83+
objectName: objectName,
84+
client: client,
85+
}
86+
87+
return a, nil
88+
}
89+
90+
func init() {
91+
err := cache.Factories().Register("s3v2", New)
92+
if err != nil {
93+
panic(err)
94+
}
95+
}

cache/s3v2/adapter_test.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
//go:build !integration
2+
3+
package s3v2
4+
5+
import (
6+
"context"
7+
"errors"
8+
"net/url"
9+
"testing"
10+
"time"
11+
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/mock"
14+
"github.com/stretchr/testify/require"
15+
16+
"gitlab.com/gitlab-org/gitlab-runner/cache"
17+
"gitlab.com/gitlab-org/gitlab-runner/common"
18+
)
19+
20+
var defaultTimeout = 1 * time.Hour
21+
22+
const (
23+
bucketName = "test"
24+
objectName = "key"
25+
bucketLocation = "location"
26+
)
27+
28+
func defaultCacheFactory() *common.CacheConfig {
29+
return &common.CacheConfig{
30+
Type: "s3v2",
31+
S3: &common.CacheS3Config{
32+
ServerAddress: "server.com",
33+
AccessKey: "access",
34+
SecretKey: "key",
35+
BucketName: bucketName,
36+
BucketLocation: bucketLocation},
37+
}
38+
}
39+
40+
type cacheOperationTest struct {
41+
errorOnS3ClientInitialization bool
42+
errorOnURLPresigning bool
43+
44+
presignedURL *url.URL
45+
expectedURL *url.URL
46+
}
47+
48+
func onFakeS3URLGenerator(tc cacheOperationTest) func() {
49+
client := new(mockS3Presigner)
50+
51+
var err error
52+
if tc.errorOnURLPresigning {
53+
err = errors.New("test error")
54+
}
55+
56+
client.
57+
On(
58+
"PresignURL", mock.Anything, mock.Anything, mock.Anything,
59+
mock.Anything, mock.Anything,
60+
).
61+
Return(tc.presignedURL, err)
62+
63+
oldS3URLGenerator := newS3Client
64+
newS3Client = func(s3 *common.CacheS3Config) (s3Presigner, error) {
65+
if tc.errorOnS3ClientInitialization {
66+
return nil, errors.New("test error")
67+
}
68+
return client, nil
69+
}
70+
71+
return func() {
72+
newS3Client = oldS3URLGenerator
73+
}
74+
}
75+
76+
func testCacheOperation(
77+
t *testing.T,
78+
operationName string,
79+
operation func(adapter cache.Adapter) *url.URL,
80+
tc cacheOperationTest,
81+
cacheConfig *common.CacheConfig,
82+
) {
83+
t.Run(operationName, func(t *testing.T) {
84+
cleanupS3URLGeneratorMock := onFakeS3URLGenerator(tc)
85+
defer cleanupS3URLGeneratorMock()
86+
87+
adapter, err := New(cacheConfig, defaultTimeout, objectName)
88+
89+
if tc.errorOnS3ClientInitialization {
90+
assert.EqualError(t, err, "error while creating S3 cache storage client: test error")
91+
92+
return
93+
}
94+
require.NoError(t, err)
95+
96+
URL := operation(adapter)
97+
assert.Equal(t, tc.expectedURL, URL)
98+
99+
uploadHeaders := adapter.GetUploadHeaders()
100+
assert.Nil(t, uploadHeaders)
101+
102+
assert.Nil(t, adapter.GetGoCloudURL(context.Background()))
103+
assert.Empty(t, adapter.GetUploadEnv())
104+
})
105+
}
106+
107+
func TestCacheOperation(t *testing.T) {
108+
URL, err := url.Parse("https://s3.example.com")
109+
require.NoError(t, err)
110+
111+
tests := map[string]cacheOperationTest{
112+
"error-on-s3-client-initialization": {
113+
errorOnS3ClientInitialization: true,
114+
},
115+
"error-on-presigning-url": {
116+
errorOnURLPresigning: true,
117+
presignedURL: URL,
118+
expectedURL: nil,
119+
},
120+
"presigned-url": {
121+
presignedURL: URL,
122+
expectedURL: URL,
123+
},
124+
}
125+
126+
for testName, test := range tests {
127+
t.Run(testName, func(t *testing.T) {
128+
testCacheOperation(
129+
t,
130+
"GetDownloadURL",
131+
func(adapter cache.Adapter) *url.URL { return adapter.GetDownloadURL(context.Background()) },
132+
test,
133+
defaultCacheFactory(),
134+
)
135+
testCacheOperation(
136+
t,
137+
"GetUploadURL",
138+
func(adapter cache.Adapter) *url.URL { return adapter.GetUploadURL(context.Background()) },
139+
test,
140+
defaultCacheFactory(),
141+
)
142+
})
143+
}
144+
}
145+
146+
func TestNoConfiguration(t *testing.T) {
147+
s3Cache := defaultCacheFactory()
148+
s3Cache.S3 = nil
149+
150+
adapter, err := New(s3Cache, defaultTimeout, objectName)
151+
assert.Nil(t, adapter)
152+
153+
assert.EqualError(t, err, "missing S3 configuration")
154+
}

cache/s3v2/mock_s3Presigner.go

Lines changed: 61 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)