github.com/lyft/flytestdlib@v0.3.12-0.20210213045714-8cdd111ecda1/storage/stow_store_test.go (about) 1 package storage 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "net/url" 10 "os" 11 "path/filepath" 12 "testing" 13 "time" 14 15 "github.com/graymeta/stow/google" 16 "github.com/graymeta/stow/local" 17 "github.com/pkg/errors" 18 19 "github.com/graymeta/stow" 20 "github.com/stretchr/testify/assert" 21 22 "github.com/lyft/flytestdlib/config" 23 "github.com/lyft/flytestdlib/contextutils" 24 "github.com/lyft/flytestdlib/internal/utils" 25 "github.com/lyft/flytestdlib/promutils" 26 "github.com/lyft/flytestdlib/promutils/labeled" 27 ) 28 29 type mockStowLoc struct { 30 stow.Location 31 ContainerCb func(id string) (stow.Container, error) 32 } 33 34 func (m mockStowLoc) Container(id string) (stow.Container, error) { 35 return m.ContainerCb(id) 36 } 37 38 type mockStowContainer struct { 39 id string 40 items map[string]mockStowItem 41 } 42 43 func (m mockStowContainer) ID() string { 44 return m.id 45 } 46 47 func (m mockStowContainer) Name() string { 48 return m.id 49 } 50 51 func (m mockStowContainer) Item(id string) (stow.Item, error) { 52 if item, found := m.items[id]; found { 53 return item, nil 54 } 55 56 return nil, stow.ErrNotFound 57 } 58 59 func (mockStowContainer) Items(prefix, cursor string, count int) ([]stow.Item, string, error) { 60 return []stow.Item{}, "", nil 61 } 62 63 func (mockStowContainer) RemoveItem(id string) error { 64 return nil 65 } 66 67 func (m *mockStowContainer) Put(name string, r io.Reader, size int64, metadata map[string]interface{}) (stow.Item, error) { 68 item := mockStowItem{url: name, size: size} 69 m.items[name] = item 70 return item, nil 71 } 72 73 func newMockStowContainer(id string) *mockStowContainer { 74 return &mockStowContainer{ 75 id: id, 76 items: map[string]mockStowItem{}, 77 } 78 } 79 80 type mockStowItem struct { 81 url string 82 size int64 83 } 84 85 func (m mockStowItem) ID() string { 86 return m.url 87 } 88 89 func (m mockStowItem) Name() string { 90 return m.url 91 } 92 93 func (m mockStowItem) URL() *url.URL { 94 u, err := url.Parse(m.url) 95 if err != nil { 96 panic(err) 97 } 98 99 return u 100 } 101 102 func (m mockStowItem) Size() (int64, error) { 103 return m.size, nil 104 } 105 106 func (mockStowItem) Open() (io.ReadCloser, error) { 107 return ioutil.NopCloser(bytes.NewReader([]byte{})), nil 108 } 109 110 func (mockStowItem) ETag() (string, error) { 111 return "", nil 112 } 113 114 func (mockStowItem) LastMod() (time.Time, error) { 115 return time.Now(), nil 116 } 117 118 func (mockStowItem) Metadata() (map[string]interface{}, error) { 119 return map[string]interface{}{}, nil 120 } 121 122 func TestStowStore_ReadRaw(t *testing.T) { 123 labeled.SetMetricKeys(contextutils.ProjectKey, contextutils.DomainKey, contextutils.WorkflowIDKey, contextutils.TaskIDKey) 124 125 const container = "container" 126 t.Run("Happy Path", func(t *testing.T) { 127 testScope := promutils.NewTestScope() 128 fn := fQNFn["s3"] 129 s, err := NewStowRawStore(fn(container), &mockStowLoc{ 130 ContainerCb: func(id string) (stow.Container, error) { 131 if id == container { 132 return newMockStowContainer(container), nil 133 } 134 return nil, fmt.Errorf("container is not supported") 135 }, 136 }, false, testScope) 137 assert.NoError(t, err) 138 err = s.WriteRaw(context.TODO(), DataReference("s3://container/path"), 0, Options{}, bytes.NewReader([]byte{})) 139 assert.NoError(t, err) 140 metadata, err := s.Head(context.TODO(), DataReference("s3://container/path")) 141 assert.NoError(t, err) 142 assert.True(t, metadata.Exists()) 143 raw, err := s.ReadRaw(context.TODO(), DataReference("s3://container/path")) 144 assert.NoError(t, err) 145 rawBytes, err := ioutil.ReadAll(raw) 146 assert.NoError(t, err) 147 assert.Equal(t, 0, len(rawBytes)) 148 assert.Equal(t, DataReference("s3://container"), s.GetBaseContainerFQN(context.TODO())) 149 }) 150 151 t.Run("Exceeds limit", func(t *testing.T) { 152 testScope := promutils.NewTestScope() 153 fn := fQNFn["s3"] 154 s, err := NewStowRawStore(fn(container), &mockStowLoc{ 155 ContainerCb: func(id string) (stow.Container, error) { 156 if id == container { 157 return newMockStowContainer(container), nil 158 } 159 return nil, fmt.Errorf("container is not supported") 160 }, 161 }, false, testScope) 162 assert.NoError(t, err) 163 err = s.WriteRaw(context.TODO(), DataReference("s3://container/path"), 3*MiB, Options{}, bytes.NewReader([]byte{})) 164 assert.NoError(t, err) 165 metadata, err := s.Head(context.TODO(), DataReference("s3://container/path")) 166 assert.NoError(t, err) 167 assert.True(t, metadata.Exists()) 168 _, err = s.ReadRaw(context.TODO(), DataReference("s3://container/path")) 169 assert.Error(t, err) 170 assert.True(t, IsExceedsLimit(err)) 171 assert.NotNil(t, errors.Cause(err)) 172 }) 173 174 t.Run("Happy Path multi-container enabled", func(t *testing.T) { 175 testScope := promutils.NewTestScope() 176 fn := fQNFn["s3"] 177 s, err := NewStowRawStore(fn(container), &mockStowLoc{ 178 ContainerCb: func(id string) (stow.Container, error) { 179 if id == container { 180 return newMockStowContainer(container), nil 181 } else if id == "bad-container" { 182 return newMockStowContainer("bad-container"), nil 183 } 184 return nil, fmt.Errorf("container is not supported") 185 }, 186 }, true, testScope) 187 assert.NoError(t, err) 188 err = s.WriteRaw(context.TODO(), "s3://bad-container/path", 0, Options{}, bytes.NewReader([]byte{})) 189 assert.NoError(t, err) 190 metadata, err := s.Head(context.TODO(), "s3://bad-container/path") 191 if assert.NoError(t, err) { 192 assert.True(t, metadata.Exists()) 193 } 194 raw, err := s.ReadRaw(context.TODO(), "s3://bad-container/path") 195 assert.NoError(t, err) 196 rawBytes, err := ioutil.ReadAll(raw) 197 assert.NoError(t, err) 198 assert.Equal(t, 0, len(rawBytes)) 199 assert.Equal(t, DataReference("s3://container"), s.GetBaseContainerFQN(context.TODO())) 200 }) 201 202 t.Run("Happy Path multi-container bad", func(t *testing.T) { 203 testScope := promutils.NewTestScope() 204 fn := fQNFn["s3"] 205 s, err := NewStowRawStore(fn(container), &mockStowLoc{ 206 ContainerCb: func(id string) (stow.Container, error) { 207 if id == container { 208 return newMockStowContainer(container), nil 209 } 210 return nil, fmt.Errorf("container is not supported") 211 }, 212 }, true, testScope) 213 assert.NoError(t, err) 214 err = s.WriteRaw(context.TODO(), "s3://bad-container/path", 0, Options{}, bytes.NewReader([]byte{})) 215 assert.Error(t, err) 216 _, err = s.Head(context.TODO(), "s3://bad-container/path") 217 assert.Error(t, err) 218 _, err = s.ReadRaw(context.TODO(), "s3://bad-container/path") 219 assert.Error(t, err) 220 }) 221 } 222 223 func TestNewLocalStore(t *testing.T) { 224 labeled.SetMetricKeys(contextutils.ProjectKey, contextutils.DomainKey, contextutils.WorkflowIDKey, contextutils.TaskIDKey) 225 t.Run("Valid config", func(t *testing.T) { 226 testScope := promutils.NewTestScope() 227 store, err := newStowRawStore(&Config{ 228 Stow: &StowConfig{ 229 Kind: local.Kind, 230 Config: map[string]string{ 231 local.ConfigKeyPath: "./", 232 }, 233 }, 234 InitContainer: "testdata", 235 }, testScope.NewSubScope("x")) 236 237 assert.NoError(t, err) 238 assert.NotNil(t, store) 239 240 // Stow local store expects the full path after the container portion (looks like a bug to me) 241 rc, err := store.ReadRaw(context.TODO(), DataReference("file://testdata/config.yaml")) 242 assert.NoError(t, err) 243 if assert.NotNil(t, rc) { 244 assert.NoError(t, rc.Close()) 245 } 246 }) 247 248 t.Run("Invalid config", func(t *testing.T) { 249 testScope := promutils.NewTestScope() 250 _, err := newStowRawStore(&Config{}, testScope) 251 assert.Error(t, err) 252 }) 253 254 t.Run("Initialize container", func(t *testing.T) { 255 testScope := promutils.NewTestScope() 256 tmpDir, err := ioutil.TempDir("", "stdlib_local") 257 assert.NoError(t, err) 258 259 stats, err := os.Stat(tmpDir) 260 assert.NoError(t, err) 261 assert.NotNil(t, stats) 262 263 store, err := newStowRawStore(&Config{ 264 Stow: &StowConfig{ 265 Kind: local.Kind, 266 Config: map[string]string{ 267 local.ConfigKeyPath: tmpDir, 268 }, 269 }, 270 InitContainer: "tmp", 271 }, testScope.NewSubScope("y")) 272 273 assert.NoError(t, err) 274 assert.NotNil(t, store) 275 276 stats, err = os.Stat(filepath.Join(tmpDir, "tmp")) 277 assert.NoError(t, err) 278 if assert.NotNil(t, stats) { 279 assert.True(t, stats.IsDir()) 280 } 281 }) 282 283 t.Run("missing init container", func(t *testing.T) { 284 testScope := promutils.NewTestScope() 285 tmpDir, err := ioutil.TempDir("", "stdlib_local") 286 assert.NoError(t, err) 287 288 stats, err := os.Stat(tmpDir) 289 assert.NoError(t, err) 290 assert.NotNil(t, stats) 291 292 store, err := newStowRawStore(&Config{ 293 Stow: &StowConfig{ 294 Kind: local.Kind, 295 Config: map[string]string{ 296 local.ConfigKeyPath: tmpDir, 297 }, 298 }, 299 }, testScope.NewSubScope("y")) 300 301 assert.Error(t, err) 302 assert.Nil(t, store) 303 }) 304 305 t.Run("multi-container enabled", func(t *testing.T) { 306 testScope := promutils.NewTestScope() 307 tmpDir, err := ioutil.TempDir("", "stdlib_local") 308 assert.NoError(t, err) 309 310 stats, err := os.Stat(tmpDir) 311 assert.NoError(t, err) 312 assert.NotNil(t, stats) 313 314 store, err := newStowRawStore(&Config{ 315 Stow: &StowConfig{ 316 Kind: local.Kind, 317 Config: map[string]string{ 318 local.ConfigKeyPath: tmpDir, 319 }, 320 }, 321 InitContainer: "tmp", 322 MultiContainerEnabled: true, 323 }, testScope.NewSubScope("y")) 324 325 assert.NoError(t, err) 326 assert.NotNil(t, store) 327 328 stats, err = os.Stat(filepath.Join(tmpDir, "tmp")) 329 assert.NoError(t, err) 330 if assert.NotNil(t, stats) { 331 assert.True(t, stats.IsDir()) 332 } 333 }) 334 } 335 336 func Test_newStowRawStore(t *testing.T) { 337 type args struct { 338 cfg *Config 339 metricsScope promutils.Scope 340 } 341 tests := []struct { 342 name string 343 args args 344 wantErr bool 345 }{ 346 {"fail", args{&Config{}, promutils.NewTestScope()}, true}, 347 {"google", args{&Config{ 348 InitContainer: "flyte", 349 Stow: &StowConfig{ 350 Kind: google.Kind, 351 Config: map[string]string{ 352 google.ConfigProjectId: "x", 353 google.ConfigScopes: "y", 354 }, 355 }, 356 }, promutils.NewTestScope()}, true}, 357 {"minio", args{&Config{ 358 Type: TypeMinio, 359 InitContainer: "some-container", 360 Connection: ConnectionConfig{ 361 Endpoint: config.URL{URL: utils.MustParseURL("http://minio:9000")}, 362 }, 363 }, promutils.NewTestScope()}, true}, 364 } 365 for _, tt := range tests { 366 t.Run(tt.name, func(t *testing.T) { 367 got, err := newStowRawStore(tt.args.cfg, tt.args.metricsScope) 368 if tt.wantErr { 369 assert.Error(t, err, "newStowRawStore() error = %v, wantErr %v", err, tt.wantErr) 370 return 371 } 372 assert.NotNil(t, got, "Expected rawstore, found nil!") 373 }) 374 } 375 }