Skip to content

Commit a4012f2

Browse files
authored
Merge pull request #596 from fluxcd/bucket-provider-interface-dev
2 parents bae1b10 + ed6c6eb commit a4012f2

File tree

10 files changed

+1769
-938
lines changed

10 files changed

+1769
-938
lines changed

controllers/bucket_controller.go

Lines changed: 303 additions & 386 deletions
Large diffs are not rendered by default.
Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
/*
2+
Copyright 2022 The Flux authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controllers
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"os"
23+
"path/filepath"
24+
"testing"
25+
"time"
26+
27+
"gotest.tools/assert"
28+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29+
30+
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
31+
)
32+
33+
type mockBucketObject struct {
34+
etag string
35+
data string
36+
}
37+
38+
type mockBucketClient struct {
39+
bucketName string
40+
objects map[string]mockBucketObject
41+
}
42+
43+
var mockNotFound = fmt.Errorf("not found")
44+
45+
func (m mockBucketClient) BucketExists(_ context.Context, name string) (bool, error) {
46+
return name == m.bucketName, nil
47+
}
48+
49+
func (m mockBucketClient) FGetObject(_ context.Context, bucket, obj, path string) (string, error) {
50+
if bucket != m.bucketName {
51+
return "", fmt.Errorf("bucket does not exist")
52+
}
53+
// tiny bit of protocol, for convenience: if asked for an object "error", then return an error.
54+
if obj == "error" {
55+
return "", fmt.Errorf("I was asked to report an error")
56+
}
57+
object, ok := m.objects[obj]
58+
if !ok {
59+
return "", mockNotFound
60+
}
61+
if err := os.WriteFile(path, []byte(object.data), os.FileMode(0660)); err != nil {
62+
return "", err
63+
}
64+
return object.etag, nil
65+
}
66+
67+
func (m mockBucketClient) ObjectIsNotFound(e error) bool {
68+
return e == mockNotFound
69+
}
70+
71+
func (m mockBucketClient) VisitObjects(_ context.Context, _ string, f func(key, etag string) error) error {
72+
for key, obj := range m.objects {
73+
if err := f(key, obj.etag); err != nil {
74+
return err
75+
}
76+
}
77+
return nil
78+
}
79+
80+
func (m mockBucketClient) Close(_ context.Context) {
81+
return
82+
}
83+
84+
func (m *mockBucketClient) addObject(key string, object mockBucketObject) {
85+
if m.objects == nil {
86+
m.objects = make(map[string]mockBucketObject)
87+
}
88+
m.objects[key] = object
89+
}
90+
91+
func (m *mockBucketClient) objectsToEtagIndex() *etagIndex {
92+
i := newEtagIndex()
93+
for k, v := range m.objects {
94+
i.Add(k, v.etag)
95+
}
96+
return i
97+
}
98+
99+
func Test_fetchEtagIndex(t *testing.T) {
100+
bucketName := "all-my-config"
101+
102+
bucket := sourcev1.Bucket{
103+
Spec: sourcev1.BucketSpec{
104+
BucketName: bucketName,
105+
Timeout: &metav1.Duration{Duration: 1 * time.Hour},
106+
},
107+
}
108+
109+
t.Run("fetches etag index", func(t *testing.T) {
110+
tmp, err := os.MkdirTemp("", "test-bucket")
111+
if err != nil {
112+
t.Fatal(err)
113+
}
114+
defer os.RemoveAll(tmp)
115+
116+
client := mockBucketClient{bucketName: bucketName}
117+
client.addObject("foo.yaml", mockBucketObject{data: "foo.yaml", etag: "etag1"})
118+
client.addObject("bar.yaml", mockBucketObject{data: "bar.yaml", etag: "etag2"})
119+
client.addObject("baz.yaml", mockBucketObject{data: "baz.yaml", etag: "etag3"})
120+
121+
index := newEtagIndex()
122+
err = fetchEtagIndex(context.TODO(), client, bucket.DeepCopy(), index, tmp)
123+
if err != nil {
124+
t.Fatal(err)
125+
}
126+
127+
assert.Equal(t, index.Len(), 3)
128+
})
129+
130+
t.Run("an error while bucket does not exist", func(t *testing.T) {
131+
tmp, err := os.MkdirTemp("", "test-bucket")
132+
if err != nil {
133+
t.Fatal(err)
134+
}
135+
defer os.RemoveAll(tmp)
136+
137+
client := mockBucketClient{bucketName: "other-bucket-name"}
138+
139+
index := newEtagIndex()
140+
err = fetchEtagIndex(context.TODO(), client, bucket.DeepCopy(), index, tmp)
141+
assert.ErrorContains(t, err, "not found")
142+
})
143+
144+
t.Run("filters with .sourceignore rules", func(t *testing.T) {
145+
tmp, err := os.MkdirTemp("", "test-bucket")
146+
if err != nil {
147+
t.Fatal(err)
148+
}
149+
defer os.RemoveAll(tmp)
150+
151+
client := mockBucketClient{bucketName: bucketName}
152+
client.addObject(".sourceignore", mockBucketObject{etag: "sourceignore1", data: `*.txt`})
153+
client.addObject("foo.yaml", mockBucketObject{etag: "etag1", data: "foo.yaml"})
154+
client.addObject("foo.txt", mockBucketObject{etag: "etag2", data: "foo.txt"})
155+
156+
index := newEtagIndex()
157+
err = fetchEtagIndex(context.TODO(), client, bucket.DeepCopy(), index, tmp)
158+
if err != nil {
159+
t.Fatal(err)
160+
}
161+
162+
if _, err := os.Stat(filepath.Join(tmp, ".sourceignore")); err != nil {
163+
t.Error(err)
164+
}
165+
166+
if ok := index.Has("foo.txt"); ok {
167+
t.Error(fmt.Errorf("expected 'foo.txt' index item to not exist"))
168+
}
169+
assert.Equal(t, index.Len(), 1)
170+
})
171+
172+
t.Run("filters with ignore rules from object", func(t *testing.T) {
173+
tmp, err := os.MkdirTemp("", "test-bucket")
174+
if err != nil {
175+
t.Fatal(err)
176+
}
177+
defer os.RemoveAll(tmp)
178+
179+
client := mockBucketClient{bucketName: bucketName}
180+
client.addObject(".sourceignore", mockBucketObject{etag: "sourceignore1", data: `*.txt`})
181+
client.addObject("foo.txt", mockBucketObject{etag: "etag1", data: "foo.txt"})
182+
183+
ignore := "!*.txt"
184+
bucket := bucket.DeepCopy()
185+
bucket.Spec.Ignore = &ignore
186+
187+
index := newEtagIndex()
188+
err = fetchEtagIndex(context.TODO(), client, bucket.DeepCopy(), index, tmp)
189+
if err != nil {
190+
t.Fatal(err)
191+
}
192+
193+
if _, err := os.Stat(filepath.Join(tmp, ".sourceignore")); err != nil {
194+
t.Error(err)
195+
}
196+
197+
assert.Equal(t, index.Len(), 1)
198+
if ok := index.Has("foo.txt"); !ok {
199+
t.Error(fmt.Errorf("expected 'foo.txt' index item to exist"))
200+
}
201+
})
202+
}
203+
204+
func Test_fetchFiles(t *testing.T) {
205+
bucketName := "all-my-config"
206+
207+
bucket := sourcev1.Bucket{
208+
Spec: sourcev1.BucketSpec{
209+
BucketName: bucketName,
210+
Timeout: &metav1.Duration{Duration: 1 * time.Hour},
211+
},
212+
}
213+
214+
t.Run("fetches files", func(t *testing.T) {
215+
tmp, err := os.MkdirTemp("", "test-bucket")
216+
if err != nil {
217+
t.Fatal(err)
218+
}
219+
defer os.RemoveAll(tmp)
220+
221+
client := mockBucketClient{bucketName: bucketName}
222+
client.addObject("foo.yaml", mockBucketObject{data: "foo.yaml", etag: "etag1"})
223+
client.addObject("bar.yaml", mockBucketObject{data: "bar.yaml", etag: "etag2"})
224+
client.addObject("baz.yaml", mockBucketObject{data: "baz.yaml", etag: "etag3"})
225+
226+
index := client.objectsToEtagIndex()
227+
228+
err = fetchIndexFiles(context.TODO(), client, bucket.DeepCopy(), index, tmp)
229+
if err != nil {
230+
t.Fatal(err)
231+
}
232+
233+
for path := range index.Index() {
234+
p := filepath.Join(tmp, path)
235+
_, err := os.Stat(p)
236+
if err != nil {
237+
t.Error(err)
238+
}
239+
}
240+
})
241+
242+
t.Run("an error while fetching returns an error for the whole procedure", func(t *testing.T) {
243+
tmp, err := os.MkdirTemp("", "test-bucket")
244+
if err != nil {
245+
t.Fatal(err)
246+
}
247+
defer os.RemoveAll(tmp)
248+
249+
client := mockBucketClient{bucketName: bucketName, objects: map[string]mockBucketObject{}}
250+
client.objects["error"] = mockBucketObject{}
251+
252+
err = fetchIndexFiles(context.TODO(), client, bucket.DeepCopy(), client.objectsToEtagIndex(), tmp)
253+
if err == nil {
254+
t.Fatal("expected error but got nil")
255+
}
256+
})
257+
258+
t.Run("a changed etag updates the index", func(t *testing.T) {
259+
tmp, err := os.MkdirTemp("", "test-bucket")
260+
if err != nil {
261+
t.Fatal(err)
262+
}
263+
defer os.RemoveAll(tmp)
264+
265+
client := mockBucketClient{bucketName: bucketName}
266+
client.addObject("foo.yaml", mockBucketObject{data: "foo.yaml", etag: "etag2"})
267+
268+
index := newEtagIndex()
269+
index.Add("foo.yaml", "etag1")
270+
err = fetchIndexFiles(context.TODO(), client, bucket.DeepCopy(), index, tmp)
271+
if err != nil {
272+
t.Fatal(err)
273+
}
274+
f := index.Get("foo.yaml")
275+
assert.Equal(t, f, "etag2")
276+
})
277+
278+
t.Run("a disappeared index entry is removed from the index", func(t *testing.T) {
279+
tmp, err := os.MkdirTemp("", "test-bucket")
280+
if err != nil {
281+
t.Fatal(err)
282+
}
283+
defer os.RemoveAll(tmp)
284+
285+
client := mockBucketClient{bucketName: bucketName}
286+
client.addObject("foo.yaml", mockBucketObject{data: "foo.yaml", etag: "etag1"})
287+
288+
index := newEtagIndex()
289+
index.Add("foo.yaml", "etag1")
290+
// Does not exist on server
291+
index.Add("bar.yaml", "etag2")
292+
293+
err = fetchIndexFiles(context.TODO(), client, bucket.DeepCopy(), index, tmp)
294+
if err != nil {
295+
t.Fatal(err)
296+
}
297+
f := index.Get("foo.yaml")
298+
assert.Equal(t, f, "etag1")
299+
assert.Check(t, !index.Has("bar.yaml"))
300+
})
301+
302+
t.Run("can fetch more than maxConcurrentFetches", func(t *testing.T) {
303+
// this will fail if, for example, the semaphore is not used correctly and blocks
304+
tmp, err := os.MkdirTemp("", "test-bucket")
305+
if err != nil {
306+
t.Fatal(err)
307+
}
308+
defer os.RemoveAll(tmp)
309+
310+
client := mockBucketClient{bucketName: bucketName}
311+
for i := 0; i < 2*maxConcurrentBucketFetches; i++ {
312+
f := fmt.Sprintf("file-%d", i)
313+
client.addObject(f, mockBucketObject{etag: f, data: f})
314+
}
315+
index := client.objectsToEtagIndex()
316+
317+
err = fetchIndexFiles(context.TODO(), client, bucket.DeepCopy(), index, tmp)
318+
if err != nil {
319+
t.Fatal(err)
320+
}
321+
})
322+
}

0 commit comments

Comments
 (0)