diff --git a/quotas/quota.go b/quotas/quota.go new file mode 100644 index 0000000..d1c249b --- /dev/null +++ b/quotas/quota.go @@ -0,0 +1,161 @@ +package quotas + +import ( + "context" + "errors" + "path" + "strconv" + + "github.com/brinkmanlab/blend4go" +) + +type defaultQuotaAssociation string +type quotaOperation string + +const ( + UnregisteredUsers defaultQuotaAssociation = "unregistered" + RegisteredUsers defaultQuotaAssociation = "registered" + NotDefault defaultQuotaAssociation = "no" + IncreaseBy quotaOperation = "+" + DecreaseBy quotaOperation = "-" + SetTo quotaOperation = "=" + Unlimited = "unlimited" +) + +type Quota struct { + galaxyInstance *blend4go.GalaxyInstance + Id blend4go.GalaxyID `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Bytes uint64 `json:"bytes,omitempty"` + Operation quotaOperation `json:"operation,omitempty"` + Default defaultQuotaAssociation `json:"-"` + Description string `json:"description,omitempty"` + DisplayAmount string `json:"display_amount,omitempty"` + Users []blend4go.GalaxyID `json:"users,omitempty"` + Groups []blend4go.GalaxyID `json:"groups,omitempty"` + Deleted bool `json:"-"` + RawDefaults []map[string]string `json:"default,omitempty"` +} + +// NewQuota creates a new quota for groups or users +// operation - may be forced to SetTo depending on other parameters +func NewQuota(ctx context.Context, g *blend4go.GalaxyInstance, name, amount, description string, operation quotaOperation, users, groups []string, default_for defaultQuotaAssociation) (*Quota, error) { + if name == "" || description == "" { + return nil, errors.New("name and description required") + } + if amount == "" { + return nil, errors.New("invalid amount") + } + if default_for != NotDefault || amount == "unlimited" || amount == "none" || amount == "no limit" { + operation = SetTo + } + //POST /api/quotas + if res, err := g.R(ctx).SetResult(&Quota{galaxyInstance: g, Default: NotDefault}).SetBody(map[string]interface{}{ + "name": name, + "amount": amount, + "operation": operation, + "description": description, + "in_users": users, + "in_groups": groups, + "default": default_for, + }).Post(BasePath); err == nil { + if result, err := blend4go.HandleResponse(res); err == nil { + quota := result.(*Quota) + quota.populateDefault() + return quota, nil + } else { + return nil, err + } + } else { + return nil, err + } +} + +func (q *Quota) populateDefault() { + q.Default = NotDefault + for _, d := range q.RawDefaults { + q.Default = defaultQuotaAssociation(d["type"]) + } +} + +func (q *Quota) GetBasePath() string { + if q.Deleted { + return path.Join(BasePath, "deleted") + } + return BasePath +} + +func (q *Quota) SetGalaxyInstance(instance *blend4go.GalaxyInstance) { + q.galaxyInstance = instance +} + +func (q *Quota) GetID() blend4go.GalaxyID { + return q.Id +} + +func (q *Quota) SetID(id blend4go.GalaxyID) { + q.Id = id +} + +// Update changes, sending to server +// amount - optional amount same as NewQuota, Quota.Bytes used if left empty +func (q *Quota) Update(ctx context.Context, amount string) error { + // PUT /api/quotas/{encoded_quota_id} + if amount == "" { + amount = strconv.FormatUint(q.Bytes, 10) + } + _, err := q.galaxyInstance.R(ctx).SetBody(map[string]interface{}{ + "name": q.Name, + "amount": amount, + "operation": q.Operation, + "description": q.Description, + "in_users": q.Users, + "in_groups": q.Groups, + "default": q.Default, + }).Put(path.Join(q.GetBasePath(), q.Id)) + if err == nil { + _, err = get(ctx, q.galaxyInstance, q) + } + return err +} + +// Delete quota +func (q *Quota) Delete(ctx context.Context, purge bool) error { + // DELETE /api/quotas/{encoded_quota_id} + if q.Default != NotDefault { + // unset default first + q.Default = NotDefault + if err := q.Update(ctx, ""); err != nil { + return err + } + } + params := map[string]string{} + if purge && false { // TODO https://github.com/galaxyproject/galaxy/issues/11975 + params["purge"] = "true" + // Must delete before purge request + if err := q.galaxyInstance.Delete(ctx, q, nil); err != nil { + return err + } + q.Deleted = true + } + err := q.galaxyInstance.Delete(ctx, q, ¶ms) + q.Deleted = true + return err +} + +// Undelete quota +func (q *Quota) Undelete(ctx context.Context) error { + // POST /api/quotas/deleted/{encoded_quota_id}/undelete + // TODO https://github.com/galaxyproject/galaxy/issues/11971 + if res, err := q.galaxyInstance.R(ctx).Post(path.Join(q.GetBasePath(), q.Id, "undelete")); err == nil { + if _, err := blend4go.HandleResponse(res); err == nil { + q.Deleted = false + _, err = get(ctx, q.galaxyInstance, q) + return err + } else { + return err + } + } else { + return err + } +} diff --git a/quotas/quota_test.go b/quotas/quota_test.go new file mode 100644 index 0000000..e1cb828 --- /dev/null +++ b/quotas/quota_test.go @@ -0,0 +1,501 @@ +package quotas + +import ( + "context" + "github.com/brinkmanlab/blend4go" + "path" + "reflect" + "testing" +) + +func TestNewQuota(t *testing.T) { + t.SkipNow() // Quotas cant actually be deleted, a fresh server would need to be created for each test + type args struct { + ctx context.Context + g *blend4go.GalaxyInstance + name string + amount string + description string + operation quotaOperation + users []string + groups []string + default_for defaultQuotaAssociation + } + tests := []struct { + name string + args args + success func(args, *Quota) bool + wantErr bool + }{ + { + name: "basic", + args: args{ + ctx: context.Background(), + g: galaxyInstance, + name: "test", + amount: "0", + description: "test", + operation: SetTo, + users: nil, + groups: nil, + default_for: NotDefault, + }, + success: nil, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewQuota(tt.args.ctx, tt.args.g, tt.args.name, tt.args.amount, tt.args.description, tt.args.operation, tt.args.users, tt.args.groups, tt.args.default_for) + if (err != nil) != tt.wantErr { + t.Errorf("NewQuota() error = %v, wantErr %v", err, tt.wantErr) + return + } + t.Cleanup(func() { + err = got.Delete(tt.args.ctx, true) + if err != nil { + t.Errorf("Failed to clean up created quota: %v", got) + } + }) + if !tt.success(tt.args, got) { + t.Errorf("NewQuota() got = %v, want %v", got, tt.args) + } + }) + } +} + +func TestQuota_Delete(t *testing.T) { + test_quota, err := createTestQuota() + if test_quota == nil { + t.Fatalf("test quota could not be set up: %v", err) + } + defer test_quota.Delete(context.Background(), true) + + t.Run("basic", func(t *testing.T) { + if err := test_quota.Delete(context.Background(), false); (err != nil) != false { + t.Errorf("Delete() error = %v, wantErr %v", err, false) + } + }) +} + +func TestQuota_GetBasePath(t *testing.T) { + type fields struct { + galaxyInstance *blend4go.GalaxyInstance + Id blend4go.GalaxyID + Name string + Bytes uint64 + Operation quotaOperation + Default defaultQuotaAssociation + Description string + DisplayAmount string + Users []blend4go.GalaxyID + Groups []blend4go.GalaxyID + Deleted bool + RawDefaults []map[string]string + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "basic", + fields: fields{Deleted: false}, + want: BasePath, + }, { + name: "deleted", + fields: fields{Deleted: true}, + want: path.Join(BasePath, "deleted"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + q := &Quota{ + galaxyInstance: tt.fields.galaxyInstance, + Id: tt.fields.Id, + Name: tt.fields.Name, + Bytes: tt.fields.Bytes, + Operation: tt.fields.Operation, + Default: tt.fields.Default, + Description: tt.fields.Description, + DisplayAmount: tt.fields.DisplayAmount, + Users: tt.fields.Users, + Groups: tt.fields.Groups, + Deleted: tt.fields.Deleted, + RawDefaults: tt.fields.RawDefaults, + } + if got := q.GetBasePath(); got != tt.want { + t.Errorf("GetBasePath() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestQuota_GetID(t *testing.T) { + type fields struct { + galaxyInstance *blend4go.GalaxyInstance + Id blend4go.GalaxyID + Name string + Bytes uint64 + Operation quotaOperation + Default defaultQuotaAssociation + Description string + DisplayAmount string + Users []blend4go.GalaxyID + Groups []blend4go.GalaxyID + Deleted bool + RawDefaults []map[string]string + } + tests := []struct { + name string + fields fields + want blend4go.GalaxyID + }{ + { + name: "basic", + fields: fields{Id: "test"}, + want: "test", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + q := &Quota{ + galaxyInstance: tt.fields.galaxyInstance, + Id: tt.fields.Id, + Name: tt.fields.Name, + Bytes: tt.fields.Bytes, + Operation: tt.fields.Operation, + Default: tt.fields.Default, + Description: tt.fields.Description, + DisplayAmount: tt.fields.DisplayAmount, + Users: tt.fields.Users, + Groups: tt.fields.Groups, + Deleted: tt.fields.Deleted, + RawDefaults: tt.fields.RawDefaults, + } + if got := q.GetID(); got != tt.want { + t.Errorf("GetID() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestQuota_SetGalaxyInstance(t *testing.T) { + type fields struct { + galaxyInstance *blend4go.GalaxyInstance + Id blend4go.GalaxyID + Name string + Bytes uint64 + Operation quotaOperation + Default defaultQuotaAssociation + Description string + DisplayAmount string + Users []blend4go.GalaxyID + Groups []blend4go.GalaxyID + Deleted bool + RawDefaults []map[string]string + } + type args struct { + instance *blend4go.GalaxyInstance + } + tests := []struct { + name string + fields fields + args args + want *blend4go.GalaxyInstance + }{ + { + name: "basic", + fields: fields{}, + args: args{ + instance: galaxyInstance, + }, + want: galaxyInstance, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + q := &Quota{ + galaxyInstance: tt.fields.galaxyInstance, + Id: tt.fields.Id, + Name: tt.fields.Name, + Bytes: tt.fields.Bytes, + Operation: tt.fields.Operation, + Default: tt.fields.Default, + Description: tt.fields.Description, + DisplayAmount: tt.fields.DisplayAmount, + Users: tt.fields.Users, + Groups: tt.fields.Groups, + Deleted: tt.fields.Deleted, + RawDefaults: tt.fields.RawDefaults, + } + q.SetGalaxyInstance(tt.args.instance) + if q.galaxyInstance != tt.want { + t.Errorf("GetID() = %v, want %v", q.galaxyInstance, tt.want) + } + }) + } +} + +func TestQuota_SetID(t *testing.T) { + type fields struct { + galaxyInstance *blend4go.GalaxyInstance + Id blend4go.GalaxyID + Name string + Bytes uint64 + Operation quotaOperation + Default defaultQuotaAssociation + Description string + DisplayAmount string + Users []blend4go.GalaxyID + Groups []blend4go.GalaxyID + Deleted bool + RawDefaults []map[string]string + } + type args struct { + id blend4go.GalaxyID + } + tests := []struct { + name string + fields fields + args args + want blend4go.GalaxyID + }{ + { + name: "basic", + fields: fields{}, + args: args{id: "test"}, + want: "test", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + q := &Quota{ + galaxyInstance: tt.fields.galaxyInstance, + Id: tt.fields.Id, + Name: tt.fields.Name, + Bytes: tt.fields.Bytes, + Operation: tt.fields.Operation, + Default: tt.fields.Default, + Description: tt.fields.Description, + DisplayAmount: tt.fields.DisplayAmount, + Users: tt.fields.Users, + Groups: tt.fields.Groups, + Deleted: tt.fields.Deleted, + RawDefaults: tt.fields.RawDefaults, + } + q.SetID(tt.args.id) + if q.Id != tt.want { + t.Errorf("GetID() = %v, want %v", q.Id, tt.want) + } + }) + } +} + +func TestQuota_Undelete(t *testing.T) { + test_quota, err := createTestQuota() + if test_quota == nil { + t.Fatalf("test quota could not be set up: %v", err) + } + defer test_quota.Delete(context.Background(), true) + + type args struct { + ctx context.Context + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "basic", + args: args{ctx: context.Background()}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + test_quota.Delete(tt.args.ctx, false) + if err := test_quota.Undelete(tt.args.ctx); (err != nil) != tt.wantErr { + t.Errorf("Undelete() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestQuota_Update(t *testing.T) { + test_quota, err := createTestQuota() + if test_quota == nil { + t.Fatalf("test quota could not be set up: %v", err) + } + defer test_quota.Delete(context.Background(), true) + type fields struct { + Bytes uint64 + Operation quotaOperation + Default defaultQuotaAssociation + Description string + Users []blend4go.GalaxyID + Groups []blend4go.GalaxyID + } + type args struct { + ctx context.Context + amount string + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + { + name: "basic", + fields: fields{ + Bytes: 100, + Operation: IncreaseBy, + Default: NotDefault, + Description: "test_basic", + Users: []string{}, + Groups: []string{}, + }, + args: args{ + ctx: context.Background(), + amount: "1G", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + q := &Quota{ + galaxyInstance: test_quota.galaxyInstance, + Id: test_quota.Id, + Name: test_quota.Name, + Bytes: tt.fields.Bytes, + Operation: tt.fields.Operation, + Default: tt.fields.Default, + Description: tt.fields.Description, + DisplayAmount: test_quota.DisplayAmount, + Users: tt.fields.Users, + Groups: tt.fields.Groups, + Deleted: test_quota.Deleted, + RawDefaults: test_quota.RawDefaults, + } + test_quota.Bytes = tt.fields.Bytes + test_quota.Operation = tt.fields.Operation + test_quota.Default = tt.fields.Default + test_quota.Description = tt.fields.Description + test_quota.Users = tt.fields.Users + test_quota.Groups = tt.fields.Groups + if err := test_quota.Update(tt.args.ctx, tt.args.amount); (err != nil) != tt.wantErr { + t.Errorf("Update() error = %v, wantErr %v", err, tt.wantErr) + } + if tt.args.amount != "" { + q.Bytes = test_quota.Bytes + } + q.DisplayAmount = test_quota.DisplayAmount + if !reflect.DeepEqual(test_quota, q) { + t.Errorf("Update()\ngot = %#v\nwant= %#v", test_quota, q) + } + }) + } +} + +func TestQuota_populateDefault(t *testing.T) { + type fields struct { + Default defaultQuotaAssociation + RawDefaults []map[string]string + } + tests := []struct { + name string + fields fields + want defaultQuotaAssociation + }{ + { + name: "empty", + fields: fields{ + Default: "", + RawDefaults: []map[string]string{{"type": string(NotDefault)}}, + }, + want: NotDefault, + }, { + name: "overwrite", + fields: fields{ + Default: RegisteredUsers, + RawDefaults: []map[string]string{{"type": string(NotDefault)}}, + }, + want: NotDefault, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + q := &Quota{ + Default: tt.fields.Default, + RawDefaults: tt.fields.RawDefaults, + } + q.populateDefault() + if q.Default != tt.want { + t.Errorf("populateDefault() got = %v, want %v", q.Default, tt.want) + } + }) + } +} + +func Test_get(t *testing.T) { + test_quota, err := createTestQuota() + if test_quota == nil { + t.Fatalf("test quota could not be set up: %v", err) + } + defer test_quota.Delete(context.Background(), true) + type args struct { + ctx context.Context + g *blend4go.GalaxyInstance + model *Quota + } + tests := []struct { + name string + args args + want *Quota + wantErr bool + }{ + { + name: "unpopulated", + args: args{ + ctx: context.Background(), + g: galaxyInstance, + model: &Quota{Id: test_quota.Id, Deleted: test_quota.Deleted}, + }, + want: test_quota, + wantErr: false, + }, { + name: "populated", + args: args{ + ctx: context.Background(), + g: galaxyInstance, + model: test_quota, + }, + want: test_quota, + wantErr: false, + }, { + name: "not_exist", + args: args{ + ctx: context.Background(), + g: galaxyInstance, + model: &Quota{}, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := get(tt.args.ctx, tt.args.g, tt.args.model) + if (err != nil) != tt.wantErr { + t.Errorf("get() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("get() got = %+v, want %+v", got, tt.want) + } + }) + } +} diff --git a/quotas/quotas.go b/quotas/quotas.go new file mode 100644 index 0000000..3f9e7ab --- /dev/null +++ b/quotas/quotas.go @@ -0,0 +1,64 @@ +// quotas models represent and manipulate quotas within a Galaxy instance +// Relevant api endpoints are: `/api/quotas` +package quotas + +import ( + "context" + "path" + + "github.com/brinkmanlab/blend4go" +) + +const BasePath = "/api/quotas" + +// get full quota, populating model +func get(ctx context.Context, g *blend4go.GalaxyInstance, model *Quota) (*Quota, error) { + // GET /api/quotas/{encoded_id} GET /api/quotas/deleted/{encoded_id} + if res, err := g.Get(ctx, model.GetID(), model, nil); err == nil { + quota := res.(*Quota) + quota.populateDefault() + return quota, nil + } else { + return nil, err + } +} + +// List quotas +// deleted - If true, show deleted quotas +func List(ctx context.Context, g *blend4go.GalaxyInstance, deleted bool) ([]*Quota, error) { + fullpath := BasePath + if deleted { + fullpath = path.Join(BasePath, "deleted") + } + // GET /api/quotas GET /api/quotas/deleted + var quotas []*Quota + _, err := g.List(ctx, fullpath, "as, nil) + for _, quota := range quotas { + quota.Deleted = deleted + quota, err = get(ctx, g, quota) + if err != nil { + return nil, err + } + quota.populateDefault() + } + return quotas, err +} + +// Get quota +func Get(ctx context.Context, g *blend4go.GalaxyInstance, id blend4go.GalaxyID, deleted bool) (*Quota, error) { + return get(ctx, g, &Quota{Id: id, Deleted: deleted}) +} + +// GetName get quota by name +func GetName(ctx context.Context, g *blend4go.GalaxyInstance, name string) (*Quota, error) { + for _, deleted := range []bool{true, false} { + if quotas, err := List(ctx, g, deleted); err == nil { + for _, quota := range quotas { + if quota.Name == name { + return quota, nil + } + } + } + } + return nil, nil +} diff --git a/quotas/quotas_test.go b/quotas/quotas_test.go new file mode 100644 index 0000000..be60349 --- /dev/null +++ b/quotas/quotas_test.go @@ -0,0 +1,202 @@ +package quotas + +import ( + "context" + "github.com/brinkmanlab/blend4go" + "github.com/brinkmanlab/blend4go/test_util" + "reflect" + "testing" +) + +var galaxyInstance = test_util.NewTestInstance() + +func createTestQuota() (test_quota *Quota, err error) { + test_quota, err = NewQuota(context.Background(), galaxyInstance, "test", "0", "test", SetTo, nil, nil, NotDefault) + if err != nil { + test_quota, err = GetName(context.Background(), galaxyInstance, "test") + } + if test_quota != nil && test_quota.Deleted { + err = test_quota.Undelete(context.Background()) + } + return +} + +func TestGet(t *testing.T) { + test_quota, err := createTestQuota() + if test_quota == nil { + t.Fatalf("test quota could not be set up: %v", err) + } + defer test_quota.Delete(context.Background(), true) + type args struct { + ctx context.Context + g *blend4go.GalaxyInstance + id blend4go.GalaxyID + deleted bool + } + tests := []struct { + name string + args args + want *Quota + wantErr bool + }{ + { + name: "basic", + args: args{ + ctx: context.Background(), + g: galaxyInstance, + id: test_quota.Id, + deleted: false, + }, + want: test_quota, + wantErr: false, + }, { + name: "not_deleted", + args: args{ + ctx: context.Background(), + g: galaxyInstance, + id: test_quota.Id, + deleted: true, + }, + want: nil, + wantErr: true, + }, { + name: "not_exist", + args: args{ + ctx: context.Background(), + g: galaxyInstance, + id: "fake", + deleted: false, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Get(tt.args.ctx, tt.args.g, tt.args.id, tt.args.deleted) + if (err != nil) != tt.wantErr { + t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Get()\ngot = %#v\nwant %#v", got, tt.want) + } + }) + } +} + +func TestList(t *testing.T) { + test_quota, err := createTestQuota() + if test_quota == nil { + t.Fatalf("test quota could not be set up: %v", err) + } + defer test_quota.Delete(context.Background(), true) + type args struct { + ctx context.Context + g *blend4go.GalaxyInstance + deleted bool + } + tests := []struct { + name string + args args + success func([]*Quota) bool + wantErr bool + }{ + { + name: "basic", + args: args{ + ctx: context.Background(), + g: galaxyInstance, + deleted: false, + }, + success: func(quotas []*Quota) bool { + for _, quota := range quotas { + if quota.Deleted == true { + return false + } + } + return true + }, + wantErr: false, + }, { + name: "deleted", + args: args{ + ctx: context.Background(), + g: galaxyInstance, + deleted: true, + }, + success: func(quotas []*Quota) bool { + for _, quota := range quotas { + if quota.Deleted != true { + return false + } + } + return true + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := List(tt.args.ctx, tt.args.g, tt.args.deleted) + if (err != nil) != tt.wantErr { + t.Errorf("List() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.success(got) { + t.Errorf("List() got = %v", got) + } + }) + } +} + +func TestGetName(t *testing.T) { + test_quota, err := createTestQuota() + if test_quota == nil { + t.Fatalf("test quota could not be set up: %v", err) + } + defer test_quota.Delete(context.Background(), true) + type args struct { + ctx context.Context + g *blend4go.GalaxyInstance + name string + } + tests := []struct { + name string + args args + want *Quota + wantErr bool + }{ + { + name: "basic", + args: args{ + ctx: context.Background(), + g: galaxyInstance, + name: "test", + }, + want: test_quota, + wantErr: false, + }, { + name: "not_exist", + args: args{ + ctx: context.Background(), + g: galaxyInstance, + name: "foo_test", + }, + want: nil, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetName(tt.args.ctx, tt.args.g, tt.args.name) + if (err != nil) != tt.wantErr { + t.Errorf("GetName() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetName()\ngot = %#v\nwant %#v", got, tt.want) + } + }) + } +}