Skip to content

Commit 3448e1a

Browse files
authored
Merge pull request #1201 from savirg/custom-perf
Add support for custom performance
2 parents 1b69c6c + 08b7635 commit 3448e1a

File tree

9 files changed

+1154
-38
lines changed

9 files changed

+1154
-38
lines changed

pkg/cloud_provider/file/fake.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,16 @@ func (manager *fakeServiceManager) ResizeInstance(ctx context.Context, obj *Serv
159159
return instance, nil
160160
}
161161

162+
func (manager *fakeServiceManager) UpdateInstancePerformance(ctx context.Context, obj *ServiceInstance, perfConfig *PerformanceConfig) error {
163+
if perfConfig == nil {
164+
return fmt.Errorf("performance config cannot be nil")
165+
}
166+
if perfConfig.FixedIOPS <= 0 && perfConfig.IOPSPerTB <= 0 {
167+
return fmt.Errorf("performance config must have either FixedIOPS or IOPSPerTB set")
168+
}
169+
return nil
170+
}
171+
162172
func (manager *fakeServiceManager) CreateBackup(ctx context.Context, backupInfo *BackupInfo) (*filev1beta1.Backup, error) {
163173
if backupInfo.SourceInstanceName == "" || backupInfo.SourceShare == "" || backupInfo.SourceVolumeId == "" || backupInfo.BackupURI == "" {
164174
return nil, fmt.Errorf("BackupInfo fields are not set %+v", backupInfo)
@@ -256,6 +266,14 @@ func (m *fakeBlockingServiceManager) HasOperations(ctx context.Context, obj *Ser
256266
return false, nil
257267
}
258268

269+
// UpdateInstancePerformance is a no-op in fake blocking service for testing.
270+
func (m *fakeBlockingServiceManager) UpdateInstancePerformance(ctx context.Context, obj *ServiceInstance, perfConfig *PerformanceConfig) error {
271+
execute := make(chan struct{})
272+
m.OperationUnblocker <- execute
273+
<-execute
274+
return m.fakeServiceManager.UpdateInstancePerformance(ctx, obj, perfConfig)
275+
}
276+
259277
// Multishare fake functions defined here
260278
func (manager *fakeServiceManager) GetMultishareInstance(ctx context.Context, obj *MultishareInstance) (*MultishareInstance, error) {
261279
instance, ok := manager.createdMultishareInstance[obj.Name]

pkg/cloud_provider/file/file.go

Lines changed: 111 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ import (
4242
filev1beta1multishare "google.golang.org/api/file/v1beta1"
4343
)
4444

45+
// PerformanceConfig holds performance parameters for a Filestore instance.
46+
type PerformanceConfig struct {
47+
FixedIOPS int64 // Fixed IOPS (input/output operations per second).
48+
IOPSPerTB int64 // IOPS per TiB for density-based provisioning.
49+
}
50+
4551
const (
4652
testEndpoint = "test-file.sandbox.googleapis.com"
4753
stagingEndpoint = "staging-file.sandbox.googleapis.com"
@@ -99,18 +105,19 @@ type ListFilter struct {
99105
}
100106

101107
type ServiceInstance struct {
102-
Project string
103-
Name string
104-
Location string
105-
Tier string
106-
Network Network
107-
Volume Volume
108-
Labels map[string]string
109-
State string
110-
KmsKeyName string
111-
BackupSource string
112-
NfsExportOptions []*NfsExportOptions
113-
Protocol string
108+
Project string
109+
Name string
110+
Location string
111+
Tier string
112+
Network Network
113+
Volume Volume
114+
Labels map[string]string
115+
State string
116+
KmsKeyName string
117+
BackupSource string
118+
NfsExportOptions []*NfsExportOptions
119+
Protocol string
120+
PerformanceConfig *PerformanceConfig
114121
}
115122

116123
type Volume struct {
@@ -166,6 +173,7 @@ type Service interface {
166173
GetInstance(ctx context.Context, obj *ServiceInstance) (*ServiceInstance, error)
167174
ListInstances(ctx context.Context, obj *ServiceInstance) ([]*ServiceInstance, error)
168175
ResizeInstance(ctx context.Context, obj *ServiceInstance) (*ServiceInstance, error)
176+
UpdateInstancePerformance(ctx context.Context, obj *ServiceInstance, perfConfig *PerformanceConfig) error
169177
GetBackup(ctx context.Context, backupUri string) (*Backup, error)
170178
CreateBackup(ctx context.Context, backupInfo *BackupInfo) (*filev1beta1.Backup, error)
171179
DeleteBackup(ctx context.Context, backupId string) error
@@ -297,7 +305,26 @@ func (manager *gcfsServiceManager) CreateInstance(ctx context.Context, obj *Serv
297305
Protocol: obj.Protocol,
298306
}
299307

300-
klog.V(4).Infof("Creating instance %q: location %v, tier %q, capacity %v, network %q, ipRange %q, connectMode %q, KmsKeyName %q, labels %v, backup source %q, protocol %v",
308+
// Add performance config if provided. Only one of FixedIOPS or IOPSPerTB
309+
// may be set at a time.
310+
if obj.PerformanceConfig != nil {
311+
if obj.PerformanceConfig.FixedIOPS > 0 && obj.PerformanceConfig.IOPSPerTB > 0 {
312+
return nil, fmt.Errorf("performance config must have only one of FixedIOPS or IOPSPerTB set")
313+
}
314+
instance.PerformanceConfig = &filev1beta1.PerformanceConfig{}
315+
if obj.PerformanceConfig.FixedIOPS > 0 {
316+
instance.PerformanceConfig.FixedIops = &filev1beta1.FixedIOPS{
317+
MaxIops: obj.PerformanceConfig.FixedIOPS,
318+
}
319+
}
320+
if obj.PerformanceConfig.IOPSPerTB > 0 {
321+
instance.PerformanceConfig.IopsPerTb = &filev1beta1.IOPSPerTB{
322+
MaxIopsPerTb: obj.PerformanceConfig.IOPSPerTB,
323+
}
324+
}
325+
}
326+
327+
klog.V(4).Infof("Creating instance %q: location %v, tier %q, capacity %v, network %q, ipRange %q, connectMode %q, KmsKeyName %q, labels %v, backup source %q, protocol %v, performance config %+v",
301328
obj.Name,
302329
obj.Location,
303330
instance.Tier,
@@ -308,7 +335,8 @@ func (manager *gcfsServiceManager) CreateInstance(ctx context.Context, obj *Serv
308335
instance.KmsKeyName,
309336
instance.Labels,
310337
instance.FileShares[0].SourceBackup,
311-
obj.Protocol)
338+
obj.Protocol,
339+
obj.PerformanceConfig)
312340
op, err := manager.instancesService.Create(locationURI(obj.Project, obj.Location), instance).InstanceId(obj.Name).Context(ctx).Do()
313341
if err != nil {
314342
klog.Errorf("CreateInstance operation failed for instance %v: %v", obj.Name, err)
@@ -356,6 +384,19 @@ func cloudInstanceToServiceInstance(instance *filev1beta1.Instance) (*ServiceIns
356384
if len(instance.Networks[0].IpAddresses) > 0 {
357385
ip = instance.Networks[0].IpAddresses[0]
358386
}
387+
// Map performance config if present
388+
var perfCfg *PerformanceConfig
389+
if instance.PerformanceConfig != nil {
390+
perf := &PerformanceConfig{}
391+
if instance.PerformanceConfig.FixedIops != nil {
392+
perf.FixedIOPS = instance.PerformanceConfig.FixedIops.MaxIops
393+
}
394+
if instance.PerformanceConfig.IopsPerTb != nil {
395+
perf.IOPSPerTB = instance.PerformanceConfig.IopsPerTb.MaxIopsPerTb
396+
}
397+
perfCfg = perf
398+
}
399+
359400
return &ServiceInstance{
360401
Project: project,
361402
Location: location,
@@ -371,11 +412,12 @@ func cloudInstanceToServiceInstance(instance *filev1beta1.Instance) (*ServiceIns
371412
ReservedIpRange: instance.Networks[0].ReservedIpRange,
372413
ConnectMode: instance.Networks[0].ConnectMode,
373414
},
374-
KmsKeyName: instance.KmsKeyName,
375-
Labels: instance.Labels,
376-
State: instance.State,
377-
BackupSource: instance.FileShares[0].SourceBackup,
378-
Protocol: instance.Protocol,
415+
KmsKeyName: instance.KmsKeyName,
416+
Labels: instance.Labels,
417+
State: instance.State,
418+
BackupSource: instance.FileShares[0].SourceBackup,
419+
Protocol: instance.Protocol,
420+
PerformanceConfig: perfCfg,
379421
}, nil
380422
}
381423

@@ -513,6 +555,56 @@ func (manager *gcfsServiceManager) ResizeInstance(ctx context.Context, obj *Serv
513555
return instance, nil
514556
}
515557

558+
// UpdateInstancePerformance updates the performance configuration of a Filestore instance.
559+
func (manager *gcfsServiceManager) UpdateInstancePerformance(ctx context.Context, obj *ServiceInstance, perfConfig *PerformanceConfig) error {
560+
if perfConfig == nil {
561+
return fmt.Errorf("performance config cannot be nil")
562+
}
563+
564+
instanceuri := instanceURI(obj.Project, obj.Location, obj.Name)
565+
566+
// Validate config and log the intended change
567+
if perfConfig.FixedIOPS <= 0 && perfConfig.IOPSPerTB <= 0 {
568+
return fmt.Errorf("performance config must have either FixedIOPS or IOPSPerTB set")
569+
}
570+
if perfConfig.FixedIOPS > 0 && perfConfig.IOPSPerTB > 0 {
571+
return fmt.Errorf("performance config must have only one of FixedIOPS or IOPSPerTB set")
572+
}
573+
574+
// Create a file instance for the Patch request with performance config
575+
betaObj := &filev1beta1.Instance{
576+
PerformanceConfig: &filev1beta1.PerformanceConfig{},
577+
}
578+
579+
if perfConfig.FixedIOPS > 0 {
580+
klog.V(4).Infof("Updating instance %q with FixedIOPS: %d", obj.Name, perfConfig.FixedIOPS)
581+
betaObj.PerformanceConfig.FixedIops = &filev1beta1.FixedIOPS{
582+
MaxIops: perfConfig.FixedIOPS,
583+
}
584+
}
585+
if perfConfig.IOPSPerTB > 0 {
586+
klog.V(4).Infof("Updating instance %q with IOPSPerTB: %d", obj.Name, perfConfig.IOPSPerTB)
587+
betaObj.PerformanceConfig.IopsPerTb = &filev1beta1.IOPSPerTB{
588+
MaxIopsPerTb: perfConfig.IOPSPerTB,
589+
}
590+
}
591+
592+
klog.V(4).Infof("Patching instance %q with performance configuration: %+v", obj.Name, perfConfig)
593+
op, err := manager.instancesService.Patch(instanceuri, betaObj).UpdateMask("performanceConfig").Context(ctx).Do()
594+
if err != nil {
595+
return fmt.Errorf("patch operation failed for performance update: %w", err)
596+
}
597+
598+
klog.V(4).Infof("For instance %s, waiting for performance update op %v to complete", instanceuri, op.Name)
599+
err = manager.waitForOp(ctx, op)
600+
if err != nil {
601+
return fmt.Errorf("WaitFor performance update op %s failed: %w", op.Name, err)
602+
}
603+
604+
klog.Infof("Successfully updated performance configuration for instance %q", obj.Name)
605+
return nil
606+
}
607+
516608
func (manager *gcfsServiceManager) GetBackup(ctx context.Context, backupUri string) (*Backup, error) {
517609
backup, err := manager.backupService.Get(backupUri).Context(ctx).Do()
518610
if err != nil {

pkg/cloud_provider/file/file_test.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@ package file
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
67
"net/http"
8+
"net/http/httptest"
79
"strings"
810
"testing"
911

12+
filev1beta1 "google.golang.org/api/file/v1beta1"
13+
"google.golang.org/api/option"
14+
1015
"github.com/googleapis/gax-go/v2/apierror"
1116
"google.golang.org/api/googleapi"
1217
"google.golang.org/grpc/codes"
@@ -141,6 +146,153 @@ func TestCompareInstances(t *testing.T) {
141146
}
142147
}
143148

149+
func TestUpdateInstancePerformance(t *testing.T) {
150+
ctx := context.Background()
151+
mgr := &gcfsServiceManager{}
152+
153+
// nil perfConfig -> error
154+
if err := mgr.UpdateInstancePerformance(ctx, &ServiceInstance{}, nil); err == nil {
155+
t.Fatalf("expected error for nil perfConfig")
156+
}
157+
158+
// zero values -> error
159+
if err := mgr.UpdateInstancePerformance(ctx, &ServiceInstance{}, &PerformanceConfig{}); err == nil {
160+
t.Fatalf("expected error for empty perfConfig")
161+
}
162+
163+
// success path using httptest server to mock PATCH and operations GET
164+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
165+
// return an operation for PATCH and return done=true for GET on operations
166+
if r.Method == "PATCH" && strings.Contains(r.URL.Path, "/instances/") {
167+
w.Header().Set("Content-Type", "application/json")
168+
fmt.Fprint(w, `{"name":"projects/proj/locations/loc/operations/op1","done":false}`)
169+
return
170+
}
171+
if r.Method == "GET" && strings.Contains(r.URL.Path, "/operations/") {
172+
w.Header().Set("Content-Type", "application/json")
173+
fmt.Fprint(w, `{"name":"projects/proj/locations/loc/operations/op1","done":true}`)
174+
return
175+
}
176+
http.NotFound(w, r)
177+
}))
178+
defer ts.Close()
179+
180+
// create file service pointing to test server
181+
svc, err := filev1beta1.NewService(ctx, option.WithEndpoint(ts.URL+"/"), option.WithHTTPClient(ts.Client()))
182+
if err != nil {
183+
t.Fatalf("failed to create file service: %v", err)
184+
}
185+
186+
mgr.instancesService = filev1beta1.NewProjectsLocationsInstancesService(svc)
187+
mgr.operationsService = filev1beta1.NewProjectsLocationsOperationsService(svc)
188+
189+
si := &ServiceInstance{Project: "proj", Location: "loc", Name: "name"}
190+
perf := &PerformanceConfig{FixedIOPS: 100}
191+
192+
if err := mgr.UpdateInstancePerformance(ctx, si, perf); err != nil {
193+
t.Fatalf("expected success, got error: %v", err)
194+
}
195+
}
196+
197+
func TestCreateInstance_PerformanceConfig(t *testing.T) {
198+
ctx := context.Background()
199+
mgr := &gcfsServiceManager{}
200+
201+
// Mock server to capture Create request and return operation and instance
202+
var captured filev1beta1.Instance
203+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
204+
if r.Method == "POST" && strings.Contains(r.URL.Path, "/instances") {
205+
if err := json.NewDecoder(r.Body).Decode(&captured); err != nil {
206+
http.Error(w, err.Error(), http.StatusBadRequest)
207+
return
208+
}
209+
w.Header().Set("Content-Type", "application/json")
210+
fmt.Fprint(w, `{"name":"projects/proj/locations/loc/operations/op1","done":false}`)
211+
return
212+
}
213+
if r.Method == "GET" && strings.Contains(r.URL.Path, "/operations/") {
214+
w.Header().Set("Content-Type", "application/json")
215+
fmt.Fprint(w, `{"name":"projects/proj/locations/loc/operations/op1","done":true}`)
216+
return
217+
}
218+
if r.Method == "GET" && strings.Contains(r.URL.Path, "/instances/") {
219+
// return the instance including performance config (only FixedIops)
220+
inst := filev1beta1.Instance{
221+
Name: "projects/proj/locations/loc/instances/name",
222+
Tier: "tier",
223+
FileShares: []*filev1beta1.FileShareConfig{{Name: "vol", CapacityGb: 10}},
224+
Networks: []*filev1beta1.NetworkConfig{{Network: "net"}},
225+
PerformanceConfig: &filev1beta1.PerformanceConfig{
226+
FixedIops: &filev1beta1.FixedIOPS{MaxIops: 42},
227+
},
228+
}
229+
w.Header().Set("Content-Type", "application/json")
230+
_ = json.NewEncoder(w).Encode(&inst)
231+
return
232+
}
233+
http.NotFound(w, r)
234+
}))
235+
defer ts.Close()
236+
237+
svc, err := filev1beta1.NewService(ctx, option.WithEndpoint(ts.URL+"/"), option.WithHTTPClient(ts.Client()))
238+
if err != nil {
239+
t.Fatalf("failed to create file service: %v", err)
240+
}
241+
242+
mgr.instancesService = filev1beta1.NewProjectsLocationsInstancesService(svc)
243+
mgr.operationsService = filev1beta1.NewProjectsLocationsOperationsService(svc)
244+
245+
si := &ServiceInstance{Project: "proj", Location: "loc", Name: "name", PerformanceConfig: &PerformanceConfig{FixedIOPS: 1000}, Volume: Volume{Name: "vol"}, Network: Network{Name: "net"}}
246+
247+
_, err = mgr.CreateInstance(ctx, si)
248+
if err != nil {
249+
t.Fatalf("CreateInstance failed: %v", err)
250+
}
251+
252+
if captured.PerformanceConfig == nil {
253+
t.Fatalf("expected PerformanceConfig to be sent in Create request")
254+
}
255+
if captured.PerformanceConfig.FixedIops == nil || captured.PerformanceConfig.FixedIops.MaxIops != 1000 {
256+
t.Fatalf("unexpected FixedIops in request: %+v", captured.PerformanceConfig.FixedIops)
257+
}
258+
if captured.PerformanceConfig.IopsPerTb != nil {
259+
t.Fatalf("did not expect IopsPerTb in request: %+v", captured.PerformanceConfig.IopsPerTb)
260+
}
261+
262+
siBoth := &ServiceInstance{Project: "proj", Location: "loc", Name: "name", PerformanceConfig: &PerformanceConfig{FixedIOPS: 1, IOPSPerTB: 2}, Volume: Volume{Name: "vol"}, Network: Network{Name: "net"}}
263+
_, err = mgr.CreateInstance(ctx, siBoth)
264+
if err == nil {
265+
t.Fatalf("expected CreateInstance to fail when both performance params set")
266+
}
267+
}
268+
269+
func TestCloudInstanceToServiceInstance_PerformanceConfig(t *testing.T) {
270+
inst := &filev1beta1.Instance{
271+
Name: fmt.Sprintf("projects/%s/locations/%s/instances/%s", "proj", "loc", "name"),
272+
Tier: "tier",
273+
FileShares: []*filev1beta1.FileShareConfig{{Name: "vol", CapacityGb: 10}},
274+
Networks: []*filev1beta1.NetworkConfig{{Network: "net", IpAddresses: []string{"1.2.3.4"}}},
275+
PerformanceConfig: &filev1beta1.PerformanceConfig{
276+
FixedIops: &filev1beta1.FixedIOPS{MaxIops: 1234},
277+
IopsPerTb: &filev1beta1.IOPSPerTB{MaxIopsPerTb: 5678},
278+
},
279+
}
280+
281+
si, err := cloudInstanceToServiceInstance(inst)
282+
if err != nil {
283+
t.Fatalf("unexpected error: %v", err)
284+
}
285+
if si.PerformanceConfig == nil {
286+
t.Fatalf("expected PerformanceConfig to be populated")
287+
}
288+
if si.PerformanceConfig.FixedIOPS != 1234 {
289+
t.Fatalf("expected FixedIOPS 1234, got %d", si.PerformanceConfig.FixedIOPS)
290+
}
291+
if si.PerformanceConfig.IOPSPerTB != 5678 {
292+
t.Fatalf("expected IOPSPerTB 5678, got %d", si.PerformanceConfig.IOPSPerTB)
293+
}
294+
}
295+
144296
func TestGetInstanceNameFromURI(t *testing.T) {
145297
cases := []struct {
146298
name string

0 commit comments

Comments
 (0)