From 37c2f9c771c256982905660990f475f234a614ce Mon Sep 17 00:00:00 2001 From: Cathy Ouyang Date: Fri, 7 Oct 2022 13:00:00 -0700 Subject: [PATCH 1/9] feat: add Autoclass support --- storage/bucket.go | 68 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/storage/bucket.go b/storage/bucket.go index d61fa93009b..5231a9f63ce 100644 --- a/storage/bucket.go +++ b/storage/bucket.go @@ -444,6 +444,11 @@ type BucketAttrs struct { // See https://cloud.google.com/storage/docs/managing-turbo-replication for // more information. RPO RPO + + // Autoclass holds the bucket's autoclass configuration. If enabled, + // allows for the automatic selection of the best storage class + // based on object access patterns. See for more information. + Autoclass *Autoclass } // BucketPolicyOnly is an alias for UniformBucketLevelAccess. @@ -710,6 +715,19 @@ type CustomPlacementConfig struct { DataLocations []string } +// Autoclass holds the bucket's autoclass configuration. If enabled, +// allows for the automatic selection of the best storage class +// based on object access patterns. See for more information. +type Autoclass struct { + // Enabled specifies whether the autoclass feature is enabled + // on the bucket. + Enabled bool + // ToggleTime is the time from which Autoclass was last toggled. + // If Autoclass is enabled when the bucket is created, the ToggleTime + // is set to the bucket creation time. This field is read-only. + ToggleTime time.Time +} + func newBucket(b *raw.Bucket) (*BucketAttrs, error) { if b == nil { return nil, nil @@ -744,6 +762,7 @@ func newBucket(b *raw.Bucket) (*BucketAttrs, error) { ProjectNumber: b.ProjectNumber, RPO: toRPO(b), CustomPlacementConfig: customPlacementFromRaw(b.CustomPlacementConfig), + Autoclass: toAutoclass(b.Autoclass), }, nil } @@ -776,6 +795,7 @@ func newBucketFromProto(b *storagepb.Bucket) *BucketAttrs { RPO: toRPOFromProto(b), CustomPlacementConfig: customPlacementFromProto(b.GetCustomPlacementConfig()), ProjectNumber: parseProjectNumber(b.GetProject()), // this can return 0 the project resource name is ID based + Autoclass: toAutoclassFromProto(b.GetAutoclass()), } } @@ -830,6 +850,7 @@ func (b *BucketAttrs) toRawBucket() *raw.Bucket { IamConfiguration: bktIAM, Rpo: b.RPO.String(), CustomPlacementConfig: b.CustomPlacementConfig.toRawCustomPlacement(), + Autoclass: b.Autoclass.toRawBucketAutoclass(), } } @@ -889,6 +910,7 @@ func (b *BucketAttrs) toProtoBucket() *storagepb.Bucket { IamConfig: bktIAM, Rpo: b.RPO.String(), CustomPlacementConfig: b.CustomPlacementConfig.toProtoCustomPlacement(), + Autoclass: b.Autoclass.toProtoBucketAutoclass(), } } @@ -1892,6 +1914,52 @@ func customPlacementFromProto(c *storagepb.Bucket_CustomPlacementConfig) *Custom return &CustomPlacementConfig{DataLocations: c.GetDataLocations()} } +func (a *Autoclass) toRawBucketAutoclass() *raw.BucketAutoclass { + if a == nil { + return nil + } + // Excluding read only field ToggleTime. + return &raw.BucketAutoclass{ + Enabled: a.Enabled, + } +} + +func (a *Autoclass) toProtoBucketAutoclass() *storagepb.Bucket_Autoclass { + if a == nil { + return nil + } + // Excluding read only field ToggleTime. + return &storagepb.Bucket_Autoclass{ + Enabled: a.Enabled, + } +} + +func toAutoclass(a *raw.BucketAutoclass) *Autoclass { + if a == nil { + return nil + } + t, err := time.Parse(time.RFC3339, a.ToggleTime) + if err != nil { + return &Autoclass{ + Enabled: a.Enabled, + } + } + return &Autoclass{ + Enabled: a.Enabled, + ToggleTime: t, + } +} + +func toAutoclassFromProto(a *storagepb.Bucket_Autoclass) *Autoclass { + if a == nil { + return nil + } + return &Autoclass{ + Enabled: a.GetEnabled(), + ToggleTime: a.GetToggleTime().AsTime(), + } +} + // Objects returns an iterator over the objects in the bucket that match the // Query q. If q is nil, no filtering is done. Objects will be iterated over // lexicographically by name. From a629a8938ba7a7e543a451d2994be85339853c13 Mon Sep 17 00:00:00 2001 From: Cathy Ouyang Date: Fri, 7 Oct 2022 14:37:00 -0700 Subject: [PATCH 2/9] update bucket tests --- storage/bucket.go | 10 ++++++++++ storage/bucket_test.go | 12 ++++++++++++ 2 files changed, 22 insertions(+) diff --git a/storage/bucket.go b/storage/bucket.go index 5231a9f63ce..84ffe348f42 100644 --- a/storage/bucket.go +++ b/storage/bucket.go @@ -986,6 +986,7 @@ func (ua *BucketAttrsToUpdate) toProtoBucket() *storagepb.Bucket { Website: ua.Website.toProtoBucketWebsite(), IamConfig: bktIAM, Rpo: ua.RPO.String(), + Autoclass: ua.Autoclass.toProtoBucketAutoclass(), } } @@ -1101,6 +1102,10 @@ type BucketAttrsToUpdate struct { // more information. RPO RPO + // If set, updates the autoclass configuration of the bucket. + // See for more information. + Autoclass *Autoclass + // acl is the list of access control rules on the bucket. // It is unexported and only used internally by the gRPC client. // Library users should use ACLHandle methods directly. @@ -1214,6 +1219,11 @@ func (ua *BucketAttrsToUpdate) toRawBucket() *raw.Bucket { rb.Website = ua.Website.toRawBucketWebsite() } } + if ua.Autoclass != nil { + rb.Autoclass = &raw.BucketAutoclass{ + Enabled: ua.Autoclass.Enabled, + } + } if ua.PredefinedACL != "" { // Clear ACL or the call will fail. rb.Acl = nil diff --git a/storage/bucket_test.go b/storage/bucket_test.go index 7a53625d5f0..e5f2e49ac34 100644 --- a/storage/bucket_test.go +++ b/storage/bucket_test.go @@ -60,6 +60,7 @@ func TestBucketAttrsToRawBucket(t *testing.T) { Encryption: &BucketEncryption{DefaultKMSKeyName: "key"}, Logging: &BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"}, Website: &BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"}, + Autoclass: &Autoclass{Enabled: true}, Lifecycle: Lifecycle{ Rules: []LifecycleRule{{ Action: LifecycleAction{ @@ -163,6 +164,7 @@ func TestBucketAttrsToRawBucket(t *testing.T) { Encryption: &raw.BucketEncryption{DefaultKmsKeyName: "key"}, Logging: &raw.BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"}, Website: &raw.BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"}, + Autoclass: &raw.BucketAutoclass{Enabled: true}, Lifecycle: &raw.BucketLifecycle{ Rule: []*raw.BucketLifecycleRule{{ Action: &raw.BucketLifecycleRuleAction{ @@ -391,6 +393,7 @@ func TestBucketAttrsToUpdateToRawBucket(t *testing.T) { Logging: &BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"}, Website: &BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"}, StorageClass: "NEARLINE", + Autoclass: &Autoclass{Enabled: false}, } au.SetLabel("a", "foo") au.DeleteLabel("b") @@ -434,6 +437,7 @@ func TestBucketAttrsToUpdateToRawBucket(t *testing.T) { Logging: &raw.BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"}, Website: &raw.BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"}, StorageClass: "NEARLINE", + Autoclass: &raw.BucketAutoclass{Enabled: false}, ForceSendFields: []string{"DefaultEventBasedHold", "Lifecycle"}, } if msg := testutil.Diff(got, want); msg != "" { @@ -638,6 +642,10 @@ func TestNewBucket(t *testing.T) { Logging: &raw.BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"}, Website: &raw.BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"}, ProjectNumber: 123231313, + Autoclass: &raw.BucketAutoclass{ + Enabled: true, + ToggleTime: "2017-10-23T04:05:06Z", + }, } want := &BucketAttrs{ Name: "name", @@ -688,6 +696,10 @@ func TestNewBucket(t *testing.T) { DefaultObjectACL: nil, LocationType: "dual-region", ProjectNumber: 123231313, + Autoclass: &Autoclass{ + Enabled: true, + ToggleTime: time.Date(2017, 10, 23, 4, 5, 6, 0, time.UTC), + }, } got, err := newBucket(rb) if err != nil { From a010ca9456f9c3e448e3732804de4ebed28542cd Mon Sep 17 00:00:00 2001 From: Cathy Ouyang Date: Mon, 10 Oct 2022 14:18:16 -0700 Subject: [PATCH 3/9] update doc link --- storage/bucket.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/storage/bucket.go b/storage/bucket.go index 84ffe348f42..0028067baaa 100644 --- a/storage/bucket.go +++ b/storage/bucket.go @@ -447,7 +447,7 @@ type BucketAttrs struct { // Autoclass holds the bucket's autoclass configuration. If enabled, // allows for the automatic selection of the best storage class - // based on object access patterns. See for more information. + // based on object access patterns. Autoclass *Autoclass } @@ -717,7 +717,8 @@ type CustomPlacementConfig struct { // Autoclass holds the bucket's autoclass configuration. If enabled, // allows for the automatic selection of the best storage class -// based on object access patterns. See for more information. +// based on object access patterns. See +// https://cloud.google.com/storage/docs/using-autoclass for more information. type Autoclass struct { // Enabled specifies whether the autoclass feature is enabled // on the bucket. From 56e6ac69eb785e758b439a34b9461882d09cecae Mon Sep 17 00:00:00 2001 From: Cathy Ouyang Date: Tue, 11 Oct 2022 14:49:31 -0700 Subject: [PATCH 4/9] add integration test and address comments --- storage/bucket.go | 20 ++++++++++-------- storage/bucket_test.go | 2 +- storage/integration_test.go | 41 +++++++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 10 deletions(-) diff --git a/storage/bucket.go b/storage/bucket.go index 0028067baaa..5a049d5759b 100644 --- a/storage/bucket.go +++ b/storage/bucket.go @@ -763,7 +763,7 @@ func newBucket(b *raw.Bucket) (*BucketAttrs, error) { ProjectNumber: b.ProjectNumber, RPO: toRPO(b), CustomPlacementConfig: customPlacementFromRaw(b.CustomPlacementConfig), - Autoclass: toAutoclass(b.Autoclass), + Autoclass: toAutoclassFromRaw(b.Autoclass), }, nil } @@ -851,7 +851,7 @@ func (b *BucketAttrs) toRawBucket() *raw.Bucket { IamConfiguration: bktIAM, Rpo: b.RPO.String(), CustomPlacementConfig: b.CustomPlacementConfig.toRawCustomPlacement(), - Autoclass: b.Autoclass.toRawBucketAutoclass(), + Autoclass: b.Autoclass.toRawAutoclass(), } } @@ -911,7 +911,7 @@ func (b *BucketAttrs) toProtoBucket() *storagepb.Bucket { IamConfig: bktIAM, Rpo: b.RPO.String(), CustomPlacementConfig: b.CustomPlacementConfig.toProtoCustomPlacement(), - Autoclass: b.Autoclass.toProtoBucketAutoclass(), + Autoclass: b.Autoclass.toProtoAutoclass(), } } @@ -987,7 +987,7 @@ func (ua *BucketAttrsToUpdate) toProtoBucket() *storagepb.Bucket { Website: ua.Website.toProtoBucketWebsite(), IamConfig: bktIAM, Rpo: ua.RPO.String(), - Autoclass: ua.Autoclass.toProtoBucketAutoclass(), + Autoclass: ua.Autoclass.toProtoAutoclass(), } } @@ -1104,7 +1104,7 @@ type BucketAttrsToUpdate struct { RPO RPO // If set, updates the autoclass configuration of the bucket. - // See for more information. + // See https://cloud.google.com/storage/docs/using-autoclass for more information. Autoclass *Autoclass // acl is the list of access control rules on the bucket. @@ -1222,7 +1222,8 @@ func (ua *BucketAttrsToUpdate) toRawBucket() *raw.Bucket { } if ua.Autoclass != nil { rb.Autoclass = &raw.BucketAutoclass{ - Enabled: ua.Autoclass.Enabled, + Enabled: ua.Autoclass.Enabled, + ForceSendFields: []string{"Enabled"}, } } if ua.PredefinedACL != "" { @@ -1925,7 +1926,7 @@ func customPlacementFromProto(c *storagepb.Bucket_CustomPlacementConfig) *Custom return &CustomPlacementConfig{DataLocations: c.GetDataLocations()} } -func (a *Autoclass) toRawBucketAutoclass() *raw.BucketAutoclass { +func (a *Autoclass) toRawAutoclass() *raw.BucketAutoclass { if a == nil { return nil } @@ -1935,7 +1936,7 @@ func (a *Autoclass) toRawBucketAutoclass() *raw.BucketAutoclass { } } -func (a *Autoclass) toProtoBucketAutoclass() *storagepb.Bucket_Autoclass { +func (a *Autoclass) toProtoAutoclass() *storagepb.Bucket_Autoclass { if a == nil { return nil } @@ -1945,10 +1946,11 @@ func (a *Autoclass) toProtoBucketAutoclass() *storagepb.Bucket_Autoclass { } } -func toAutoclass(a *raw.BucketAutoclass) *Autoclass { +func toAutoclassFromRaw(a *raw.BucketAutoclass) *Autoclass { if a == nil { return nil } + // Parse and return Autoclass.ToggleTime only if available. t, err := time.Parse(time.RFC3339, a.ToggleTime) if err != nil { return &Autoclass{ diff --git a/storage/bucket_test.go b/storage/bucket_test.go index e5f2e49ac34..9497d834db2 100644 --- a/storage/bucket_test.go +++ b/storage/bucket_test.go @@ -437,7 +437,7 @@ func TestBucketAttrsToUpdateToRawBucket(t *testing.T) { Logging: &raw.BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"}, Website: &raw.BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"}, StorageClass: "NEARLINE", - Autoclass: &raw.BucketAutoclass{Enabled: false}, + Autoclass: &raw.BucketAutoclass{Enabled: false, ForceSendFields: []string{"Enabled"}}, ForceSendFields: []string{"DefaultEventBasedHold", "Lifecycle"}, } if msg := testutil.Diff(got, want); msg != "" { diff --git a/storage/integration_test.go b/storage/integration_test.go index f00f17d02b4..306d0ca00a4 100644 --- a/storage/integration_test.go +++ b/storage/integration_test.go @@ -864,6 +864,47 @@ func TestIntegration_PublicAccessPrevention(t *testing.T) { } } +func TestIntegration_Autoclass(t *testing.T) { + ctx := context.Background() + client := testConfig(ctx, t) + defer client.Close() + h := testHelper{t} + + // Create a bucket with Autoclass enabled. + bkt := client.Bucket(uidSpace.New()) + h.mustCreate(bkt, testutil.ProjID(), &BucketAttrs{Autoclass: &Autoclass{Enabled: true}}) + defer h.mustDeleteBucket(bkt) + + // Get Autoclass configuration from bucket attrs. + attrs, err := bkt.Attrs(ctx) + if err != nil { + t.Fatalf("get bucket attrs failed: %v", err) + } + var toggleTime time.Time + if attrs != nil && attrs.Autoclass != nil { + if got, want := attrs.Autoclass.Enabled, true; got != want { + t.Fatalf("attr.Autoclass.Enabled = %v, want %v", got, want) + } + if toggleTime = attrs.Autoclass.ToggleTime; toggleTime.IsZero() { + t.Fatal("got a zero time value, want a populated value") + } + } + + // Disable Autoclass on the bucket. + ua := BucketAttrsToUpdate{Autoclass: &Autoclass{Enabled: false}} + attrs = h.mustUpdateBucket(bkt, ua, attrs.MetaGeneration) + if got, want := attrs.Autoclass.Enabled, false; got != want { + t.Fatalf("attr.Autoclass.Enabled = %v, want %v", got, want) + } + latestToggleTime := attrs.Autoclass.ToggleTime + if latestToggleTime.IsZero() { + t.Fatal("got a zero time value, want a populated value") + } + if latestToggleTime.Before(toggleTime) { + t.Fatal("latestToggleTime should be newer than bucket creation toggleTime") + } +} + func TestIntegration_ConditionalDelete(t *testing.T) { multiTransportTest(context.Background(), t, func(t *testing.T, ctx context.Context, bucket string, _ string, client *Client) { h := testHelper{t} From 8a77c214979790fc240e7d4e77b823d6670aaf90 Mon Sep 17 00:00:00 2001 From: Cathy Ouyang Date: Tue, 11 Oct 2022 16:41:38 -0700 Subject: [PATCH 5/9] add test coverage for proto converters --- storage/bucket_test.go | 316 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 316 insertions(+) diff --git a/storage/bucket_test.go b/storage/bucket_test.go index 9497d834db2..f95ff0c2ea8 100644 --- a/storage/bucket_test.go +++ b/storage/bucket_test.go @@ -20,11 +20,13 @@ import ( "time" "cloud.google.com/go/internal/testutil" + storagepb "cloud.google.com/go/storage/internal/apiv2/stubs" "github.com/google/go-cmp/cmp" gax "github.com/googleapis/gax-go/v2" "golang.org/x/oauth2/google" "google.golang.org/api/googleapi" raw "google.golang.org/api/storage/v1" + "google.golang.org/protobuf/proto" ) func TestBucketAttrsToRawBucket(t *testing.T) { @@ -710,6 +712,320 @@ func TestNewBucket(t *testing.T) { } } +func TestNewBucketFromProto(t *testing.T) { + pb := &storagepb.Bucket{ + Name: "name", + Acl: []*storagepb.BucketAccessControl{ + {Entity: "bob@example.com", Role: "OWNER"}, + }, + DefaultObjectAcl: []*storagepb.ObjectAccessControl{ + {Entity: "allUsers", Role: "READER"}, + }, + Location: "loc", + StorageClass: "class", + RetentionPolicy: &storagepb.Bucket_RetentionPolicy{ + RetentionPeriod: proto.Int64(int64(3)), + }, + IamConfig: &storagepb.Bucket_IamConfig{ + UniformBucketLevelAccess: &storagepb.Bucket_IamConfig_UniformBucketLevelAccess{ + Enabled: true, + }, + PublicAccessPrevention: "enforced", + }, + Rpo: rpoAsyncTurbo, + Metageneration: int64(39), + CreateTime: toProtoTimestamp(time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)), + Labels: map[string]string{"label": "value"}, + Cors: []*storagepb.Bucket_Cors{ + { + MaxAgeSeconds: 3600, + Method: []string{"GET", "POST"}, + Origin: []string{"*"}, + ResponseHeader: []string{"FOO"}, + }, + }, + Encryption: &storagepb.Bucket_Encryption{DefaultKmsKey: "key"}, + Logging: &storagepb.Bucket_Logging{LogBucket: "projects/_/buckets/lb", LogObjectPrefix: "p"}, + Website: &storagepb.Bucket_Website{MainPageSuffix: "mps", NotFoundPage: "404"}, + Autoclass: &storagepb.Bucket_Autoclass{Enabled: true, ToggleTime: toProtoTimestamp(time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC))}, + Lifecycle: &storagepb.Bucket_Lifecycle{ + Rule: []*storagepb.Bucket_Lifecycle_Rule{ + { + Action: &storagepb.Bucket_Lifecycle_Rule_Action{Type: "Delete"}, + Condition: &storagepb.Bucket_Lifecycle_Rule_Condition{ + AgeDays: proto.Int32(int32(10)), + }, + }, + }, + }, + } + want := &BucketAttrs{ + Name: "name", + ACL: []ACLRule{{Entity: "bob@example.com", Role: RoleOwner}}, + DefaultObjectACL: []ACLRule{{Entity: AllUsers, Role: RoleReader}}, + Location: "loc", + StorageClass: "class", + RetentionPolicy: &RetentionPolicy{ + RetentionPeriod: 3 * time.Second, + EffectiveTime: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC), + }, + BucketPolicyOnly: BucketPolicyOnly{Enabled: true, LockedTime: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)}, + UniformBucketLevelAccess: UniformBucketLevelAccess{Enabled: true, LockedTime: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)}, + PublicAccessPrevention: PublicAccessPreventionEnforced, + RPO: RPOAsyncTurbo, + MetaGeneration: 39, + Created: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC), + Labels: map[string]string{"label": "value"}, + CORS: []CORS{ + { + MaxAge: time.Hour, + Methods: []string{"GET", "POST"}, + Origins: []string{"*"}, + ResponseHeaders: []string{"FOO"}, + }, + }, + Encryption: &BucketEncryption{DefaultKMSKeyName: "key"}, + Logging: &BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"}, + Website: &BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"}, + Autoclass: &Autoclass{Enabled: true, ToggleTime: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)}, + Lifecycle: Lifecycle{ + Rules: []LifecycleRule{{ + Action: LifecycleAction{ + Type: DeleteAction, + }, + Condition: LifecycleCondition{ + AgeInDays: 10, + }, + }}, + }, + } + got := newBucketFromProto(pb) + if diff := testutil.Diff(got, want); diff != "" { + t.Errorf("got=-, want=+:\n%s", diff) + } +} + +func TestBucketAttrsToProtoBucket(t *testing.T) { + t.Parallel() + attrs := &BucketAttrs{ + Name: "name", + ACL: []ACLRule{{Entity: "bob@example.com", Role: RoleOwner, Domain: "d", Email: "e"}}, + DefaultObjectACL: []ACLRule{{Entity: AllUsers, Role: RoleReader, EntityID: "eid", + ProjectTeam: &ProjectTeam{ProjectNumber: "17", Team: "t"}}}, + Etag: "Zkyw9ACJZUvcYmlFaKGChzhmtnE/dt1zHSfweiWpwzdGsqXwuJZqiD0", + Location: "loc", + StorageClass: "class", + RetentionPolicy: &RetentionPolicy{ + RetentionPeriod: 3 * time.Second, + }, + BucketPolicyOnly: BucketPolicyOnly{Enabled: true}, + UniformBucketLevelAccess: UniformBucketLevelAccess{Enabled: true}, + PublicAccessPrevention: PublicAccessPreventionEnforced, + VersioningEnabled: false, + RPO: RPOAsyncTurbo, + // should be ignored: + MetaGeneration: 39, + Created: time.Now(), + Labels: map[string]string{"label": "value"}, + CORS: []CORS{ + { + MaxAge: time.Hour, + Methods: []string{"GET", "POST"}, + Origins: []string{"*"}, + ResponseHeaders: []string{"FOO"}, + }, + }, + Encryption: &BucketEncryption{DefaultKMSKeyName: "key"}, + Logging: &BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"}, + Website: &BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"}, + Autoclass: &Autoclass{Enabled: true}, + Lifecycle: Lifecycle{ + Rules: []LifecycleRule{{ + Action: LifecycleAction{ + Type: DeleteAction, + }, + Condition: LifecycleCondition{ + AgeInDays: 10, + }, + }}, + }, + } + got := attrs.toProtoBucket() + want := &storagepb.Bucket{ + Name: "name", + Acl: []*storagepb.BucketAccessControl{ + {Entity: "bob@example.com", Role: "OWNER"}, // other fields ignored on create/update + }, + DefaultObjectAcl: []*storagepb.ObjectAccessControl{ + {Entity: "allUsers", Role: "READER"}, // other fields ignored on create/update + }, + Location: "loc", + StorageClass: "class", + RetentionPolicy: &storagepb.Bucket_RetentionPolicy{ + RetentionPeriod: proto.Int64(int64(3)), + }, + IamConfig: &storagepb.Bucket_IamConfig{ + UniformBucketLevelAccess: &storagepb.Bucket_IamConfig_UniformBucketLevelAccess{ + Enabled: true, + }, + PublicAccessPrevention: "enforced", + }, + Versioning: nil, // ignore VersioningEnabled if false + Rpo: rpoAsyncTurbo, + Labels: map[string]string{"label": "value"}, + Cors: []*storagepb.Bucket_Cors{ + { + MaxAgeSeconds: 3600, + Method: []string{"GET", "POST"}, + Origin: []string{"*"}, + ResponseHeader: []string{"FOO"}, + }, + }, + Encryption: &storagepb.Bucket_Encryption{DefaultKmsKey: "key"}, + Logging: &storagepb.Bucket_Logging{LogBucket: "projects/_/buckets/lb", LogObjectPrefix: "p"}, + Website: &storagepb.Bucket_Website{MainPageSuffix: "mps", NotFoundPage: "404"}, + Autoclass: &storagepb.Bucket_Autoclass{Enabled: true}, + Lifecycle: &storagepb.Bucket_Lifecycle{ + Rule: []*storagepb.Bucket_Lifecycle_Rule{ + { + Action: &storagepb.Bucket_Lifecycle_Rule_Action{Type: "Delete"}, + Condition: &storagepb.Bucket_Lifecycle_Rule_Condition{ + AgeDays: proto.Int32(int32(10)), + NumNewerVersions: proto.Int32(int32(0)), + DaysSinceCustomTime: proto.Int32(int32(0)), + DaysSinceNoncurrentTime: proto.Int32(int32(0)), + }, + }, + }, + }, + } + + if msg := testutil.Diff(got, want); msg != "" { + t.Error(msg) + } + + attrs.VersioningEnabled = true + attrs.RequesterPays = true + got = attrs.toProtoBucket() + want.Versioning = &storagepb.Bucket_Versioning{Enabled: true} + want.Billing = &storagepb.Bucket_Billing{RequesterPays: true} + if msg := testutil.Diff(got, want); msg != "" { + t.Error(msg) + } + + // Test that setting either of BucketPolicyOnly or UniformBucketLevelAccess + // will enable UniformBucketLevelAccess. + // Set UBLA.Enabled = true --> UBLA should be set to enabled in the proto. + attrs.BucketPolicyOnly = BucketPolicyOnly{} + attrs.UniformBucketLevelAccess = UniformBucketLevelAccess{Enabled: true} + got = attrs.toProtoBucket() + want.IamConfig = &storagepb.Bucket_IamConfig{ + UniformBucketLevelAccess: &storagepb.Bucket_IamConfig_UniformBucketLevelAccess{ + Enabled: true, + }, + PublicAccessPrevention: "enforced", + } + if msg := testutil.Diff(got, want); msg != "" { + t.Errorf(msg) + } + + // Set BucketPolicyOnly.Enabled = true --> UBLA should be set to enabled in + // the proto. + attrs.BucketPolicyOnly = BucketPolicyOnly{Enabled: true} + attrs.UniformBucketLevelAccess = UniformBucketLevelAccess{} + got = attrs.toProtoBucket() + want.IamConfig = &storagepb.Bucket_IamConfig{ + UniformBucketLevelAccess: &storagepb.Bucket_IamConfig_UniformBucketLevelAccess{ + Enabled: true, + }, + PublicAccessPrevention: "enforced", + } + if msg := testutil.Diff(got, want); msg != "" { + t.Errorf(msg) + } + + // Set both BucketPolicyOnly.Enabled = true and + // UniformBucketLevelAccess.Enabled=true --> UBLA should be set to enabled + // in the proto. + attrs.BucketPolicyOnly = BucketPolicyOnly{Enabled: true} + attrs.UniformBucketLevelAccess = UniformBucketLevelAccess{Enabled: true} + got = attrs.toProtoBucket() + want.IamConfig = &storagepb.Bucket_IamConfig{ + UniformBucketLevelAccess: &storagepb.Bucket_IamConfig_UniformBucketLevelAccess{ + Enabled: true, + }, + PublicAccessPrevention: "enforced", + } + if msg := testutil.Diff(got, want); msg != "" { + t.Errorf(msg) + } + + // Set UBLA.Enabled=false and BucketPolicyOnly.Enabled=false --> UBLA + // should be disabled in the proto. + attrs.BucketPolicyOnly = BucketPolicyOnly{} + attrs.UniformBucketLevelAccess = UniformBucketLevelAccess{} + got = attrs.toProtoBucket() + want.IamConfig = &storagepb.Bucket_IamConfig{ + PublicAccessPrevention: "enforced", + } + if msg := testutil.Diff(got, want); msg != "" { + t.Errorf(msg) + } + + // Test that setting PublicAccessPrevention to "unspecified" leads to the + // inherited setting being propagated in the proto. + attrs.PublicAccessPrevention = PublicAccessPreventionUnspecified + got = attrs.toProtoBucket() + want.IamConfig = &storagepb.Bucket_IamConfig{ + PublicAccessPrevention: "inherited", + } + if msg := testutil.Diff(got, want); msg != "" { + t.Errorf(msg) + } + + // Test that setting PublicAccessPrevention to "inherited" leads to the + // setting being propagated in the proto. + attrs.PublicAccessPrevention = PublicAccessPreventionInherited + got = attrs.toProtoBucket() + want.IamConfig = &storagepb.Bucket_IamConfig{ + PublicAccessPrevention: "inherited", + } + if msg := testutil.Diff(got, want); msg != "" { + t.Errorf(msg) + } + + // Test that setting RPO to default is propagated in the proto. + attrs.RPO = RPODefault + got = attrs.toProtoBucket() + want.Rpo = rpoDefault + if msg := testutil.Diff(got, want); msg != "" { + t.Errorf(msg) + } + + // Re-enable UBLA and confirm that it does not affect the PAP setting. + attrs.UniformBucketLevelAccess = UniformBucketLevelAccess{Enabled: true} + got = attrs.toProtoBucket() + want.IamConfig = &storagepb.Bucket_IamConfig{ + UniformBucketLevelAccess: &storagepb.Bucket_IamConfig_UniformBucketLevelAccess{ + Enabled: true, + }, + PublicAccessPrevention: "inherited", + } + if msg := testutil.Diff(got, want); msg != "" { + t.Errorf(msg) + } + + // Disable UBLA and reset PAP to default. Confirm that the IAM config is set + // to nil in the proto. + attrs.UniformBucketLevelAccess = UniformBucketLevelAccess{Enabled: false} + attrs.PublicAccessPrevention = PublicAccessPreventionUnknown + got = attrs.toProtoBucket() + want.IamConfig = nil + if msg := testutil.Diff(got, want); msg != "" { + t.Errorf(msg) + } +} + func TestBucketRetryer(t *testing.T) { testCases := []struct { name string From ef7e059fd11a1ddb12cfbe2e8fa93d599eb375bb Mon Sep 17 00:00:00 2001 From: Cathy Ouyang Date: Wed, 12 Oct 2022 14:38:52 -0700 Subject: [PATCH 6/9] update tests and handle 0 time --- storage/bucket.go | 22 ++++++++++++---------- storage/bucket_test.go | 18 ++++++++++-------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/storage/bucket.go b/storage/bucket.go index 40f79dbaa4f..96f64ebb86b 100644 --- a/storage/bucket.go +++ b/storage/bucket.go @@ -737,6 +737,10 @@ func newBucket(b *raw.Bucket) (*BucketAttrs, error) { if err != nil { return nil, err } + ac, err := toAutoclassFromRaw(b.Autoclass) + if err != nil { + return nil, err + } return &BucketAttrs{ Name: b.Name, Location: b.Location, @@ -763,7 +767,7 @@ func newBucket(b *raw.Bucket) (*BucketAttrs, error) { ProjectNumber: b.ProjectNumber, RPO: toRPO(b), CustomPlacementConfig: customPlacementFromRaw(b.CustomPlacementConfig), - Autoclass: toAutoclassFromRaw(b.Autoclass), + Autoclass: ac, }, nil } @@ -1953,25 +1957,23 @@ func (a *Autoclass) toProtoAutoclass() *storagepb.Bucket_Autoclass { } } -func toAutoclassFromRaw(a *raw.BucketAutoclass) *Autoclass { - if a == nil { - return nil +func toAutoclassFromRaw(a *raw.BucketAutoclass) (*Autoclass, error) { + if a == nil || a.ToggleTime == "" { + return nil, nil } - // Parse and return Autoclass.ToggleTime only if available. + // Return Autoclass only if a valid ToggleTime is available. t, err := time.Parse(time.RFC3339, a.ToggleTime) if err != nil { - return &Autoclass{ - Enabled: a.Enabled, - } + return nil, err } return &Autoclass{ Enabled: a.Enabled, ToggleTime: t, - } + }, nil } func toAutoclassFromProto(a *storagepb.Bucket_Autoclass) *Autoclass { - if a == nil { + if a == nil || a.GetToggleTime().AsTime().Unix() == 0 { return nil } return &Autoclass{ diff --git a/storage/bucket_test.go b/storage/bucket_test.go index f95ff0c2ea8..14400731725 100644 --- a/storage/bucket_test.go +++ b/storage/bucket_test.go @@ -725,16 +725,18 @@ func TestNewBucketFromProto(t *testing.T) { StorageClass: "class", RetentionPolicy: &storagepb.Bucket_RetentionPolicy{ RetentionPeriod: proto.Int64(int64(3)), + EffectiveTime: toProtoTimestamp(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)), }, IamConfig: &storagepb.Bucket_IamConfig{ UniformBucketLevelAccess: &storagepb.Bucket_IamConfig_UniformBucketLevelAccess{ - Enabled: true, + Enabled: true, + LockTime: toProtoTimestamp(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)), }, PublicAccessPrevention: "enforced", }, Rpo: rpoAsyncTurbo, Metageneration: int64(39), - CreateTime: toProtoTimestamp(time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)), + CreateTime: toProtoTimestamp(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)), Labels: map[string]string{"label": "value"}, Cors: []*storagepb.Bucket_Cors{ { @@ -747,7 +749,7 @@ func TestNewBucketFromProto(t *testing.T) { Encryption: &storagepb.Bucket_Encryption{DefaultKmsKey: "key"}, Logging: &storagepb.Bucket_Logging{LogBucket: "projects/_/buckets/lb", LogObjectPrefix: "p"}, Website: &storagepb.Bucket_Website{MainPageSuffix: "mps", NotFoundPage: "404"}, - Autoclass: &storagepb.Bucket_Autoclass{Enabled: true, ToggleTime: toProtoTimestamp(time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC))}, + Autoclass: &storagepb.Bucket_Autoclass{Enabled: true, ToggleTime: toProtoTimestamp(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC))}, Lifecycle: &storagepb.Bucket_Lifecycle{ Rule: []*storagepb.Bucket_Lifecycle_Rule{ { @@ -767,14 +769,14 @@ func TestNewBucketFromProto(t *testing.T) { StorageClass: "class", RetentionPolicy: &RetentionPolicy{ RetentionPeriod: 3 * time.Second, - EffectiveTime: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC), + EffectiveTime: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), }, - BucketPolicyOnly: BucketPolicyOnly{Enabled: true, LockedTime: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)}, - UniformBucketLevelAccess: UniformBucketLevelAccess{Enabled: true, LockedTime: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)}, + BucketPolicyOnly: BucketPolicyOnly{Enabled: true, LockedTime: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)}, + UniformBucketLevelAccess: UniformBucketLevelAccess{Enabled: true, LockedTime: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)}, PublicAccessPrevention: PublicAccessPreventionEnforced, RPO: RPOAsyncTurbo, MetaGeneration: 39, - Created: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC), + Created: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), Labels: map[string]string{"label": "value"}, CORS: []CORS{ { @@ -787,7 +789,7 @@ func TestNewBucketFromProto(t *testing.T) { Encryption: &BucketEncryption{DefaultKMSKeyName: "key"}, Logging: &BucketLogging{LogBucket: "lb", LogObjectPrefix: "p"}, Website: &BucketWebsite{MainPageSuffix: "mps", NotFoundPage: "404"}, - Autoclass: &Autoclass{Enabled: true, ToggleTime: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)}, + Autoclass: &Autoclass{Enabled: true, ToggleTime: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)}, Lifecycle: Lifecycle{ Rules: []LifecycleRule{{ Action: LifecycleAction{ From f02bb32a3db2c9205f8a1a0459fde2b0a9b01810 Mon Sep 17 00:00:00 2001 From: Cathy Ouyang Date: Fri, 14 Oct 2022 11:17:25 -0700 Subject: [PATCH 7/9] update converter and tests --- storage/bucket.go | 18 ++++++++---------- storage/bucket_test.go | 21 ++++++++++++++------- storage/integration_test.go | 10 +++++----- 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/storage/bucket.go b/storage/bucket.go index 96f64ebb86b..28a73b8d995 100644 --- a/storage/bucket.go +++ b/storage/bucket.go @@ -737,10 +737,6 @@ func newBucket(b *raw.Bucket) (*BucketAttrs, error) { if err != nil { return nil, err } - ac, err := toAutoclassFromRaw(b.Autoclass) - if err != nil { - return nil, err - } return &BucketAttrs{ Name: b.Name, Location: b.Location, @@ -767,7 +763,7 @@ func newBucket(b *raw.Bucket) (*BucketAttrs, error) { ProjectNumber: b.ProjectNumber, RPO: toRPO(b), CustomPlacementConfig: customPlacementFromRaw(b.CustomPlacementConfig), - Autoclass: ac, + Autoclass: toAutoclassFromRaw(b.Autoclass), }, nil } @@ -1957,19 +1953,21 @@ func (a *Autoclass) toProtoAutoclass() *storagepb.Bucket_Autoclass { } } -func toAutoclassFromRaw(a *raw.BucketAutoclass) (*Autoclass, error) { +func toAutoclassFromRaw(a *raw.BucketAutoclass) *Autoclass { if a == nil || a.ToggleTime == "" { - return nil, nil + return nil } - // Return Autoclass only if a valid ToggleTime is available. + // Return Autoclass.ToggleTime only if parsed with a valid value. t, err := time.Parse(time.RFC3339, a.ToggleTime) if err != nil { - return nil, err + return &Autoclass{ + Enabled: a.Enabled, + } } return &Autoclass{ Enabled: a.Enabled, ToggleTime: t, - }, nil + } } func toAutoclassFromProto(a *storagepb.Bucket_Autoclass) *Autoclass { diff --git a/storage/bucket_test.go b/storage/bucket_test.go index 14400731725..f7557ce4135 100644 --- a/storage/bucket_test.go +++ b/storage/bucket_test.go @@ -722,6 +722,7 @@ func TestNewBucketFromProto(t *testing.T) { {Entity: "allUsers", Role: "READER"}, }, Location: "loc", + LocationType: "region", StorageClass: "class", RetentionPolicy: &storagepb.Bucket_RetentionPolicy{ RetentionPeriod: proto.Int64(int64(3)), @@ -766,6 +767,7 @@ func TestNewBucketFromProto(t *testing.T) { ACL: []ACLRule{{Entity: "bob@example.com", Role: RoleOwner}}, DefaultObjectACL: []ACLRule{{Entity: AllUsers, Role: RoleReader}}, Location: "loc", + LocationType: "region", StorageClass: "class", RetentionPolicy: &RetentionPolicy{ RetentionPeriod: 3 * time.Second, @@ -800,6 +802,11 @@ func TestNewBucketFromProto(t *testing.T) { }, }}, }, + // Populated with default values. + CustomPlacementConfig: nil, + VersioningEnabled: false, + RequesterPays: false, + ProjectNumber: 0, } got := newBucketFromProto(pb) if diff := testutil.Diff(got, want); diff != "" { @@ -814,7 +821,6 @@ func TestBucketAttrsToProtoBucket(t *testing.T) { ACL: []ACLRule{{Entity: "bob@example.com", Role: RoleOwner, Domain: "d", Email: "e"}}, DefaultObjectACL: []ACLRule{{Entity: AllUsers, Role: RoleReader, EntityID: "eid", ProjectTeam: &ProjectTeam{ProjectNumber: "17", Team: "t"}}}, - Etag: "Zkyw9ACJZUvcYmlFaKGChzhmtnE/dt1zHSfweiWpwzdGsqXwuJZqiD0", Location: "loc", StorageClass: "class", RetentionPolicy: &RetentionPolicy{ @@ -825,10 +831,8 @@ func TestBucketAttrsToProtoBucket(t *testing.T) { PublicAccessPrevention: PublicAccessPreventionEnforced, VersioningEnabled: false, RPO: RPOAsyncTurbo, - // should be ignored: - MetaGeneration: 39, - Created: time.Now(), - Labels: map[string]string{"label": "value"}, + Created: time.Now(), + Labels: map[string]string{"label": "value"}, CORS: []CORS{ { MaxAge: time.Hour, @@ -851,15 +855,18 @@ func TestBucketAttrsToProtoBucket(t *testing.T) { }, }}, }, + // Below fields should be ignored. + MetaGeneration: 39, + Etag: "Zkyw9ACJZUvcYmlFaKGChzhmtnE/dt1zHSfweiWpwzdGsqXwuJZqiD0", } got := attrs.toProtoBucket() want := &storagepb.Bucket{ Name: "name", Acl: []*storagepb.BucketAccessControl{ - {Entity: "bob@example.com", Role: "OWNER"}, // other fields ignored on create/update + {Entity: "bob@example.com", Role: "OWNER"}, }, DefaultObjectAcl: []*storagepb.ObjectAccessControl{ - {Entity: "allUsers", Role: "READER"}, // other fields ignored on create/update + {Entity: "allUsers", Role: "READER"}, }, Location: "loc", StorageClass: "class", diff --git a/storage/integration_test.go b/storage/integration_test.go index 24c33d14550..4d0d5afc659 100644 --- a/storage/integration_test.go +++ b/storage/integration_test.go @@ -874,10 +874,10 @@ func TestIntegration_Autoclass(t *testing.T) { var toggleTime time.Time if attrs != nil && attrs.Autoclass != nil { if got, want := attrs.Autoclass.Enabled, true; got != want { - t.Fatalf("attr.Autoclass.Enabled = %v, want %v", got, want) + t.Errorf("attr.Autoclass.Enabled = %v, want %v", got, want) } if toggleTime = attrs.Autoclass.ToggleTime; toggleTime.IsZero() { - t.Fatal("got a zero time value, want a populated value") + t.Error("got a zero time value, want a populated value") } } @@ -885,14 +885,14 @@ func TestIntegration_Autoclass(t *testing.T) { ua := BucketAttrsToUpdate{Autoclass: &Autoclass{Enabled: false}} attrs = h.mustUpdateBucket(bkt, ua, attrs.MetaGeneration) if got, want := attrs.Autoclass.Enabled, false; got != want { - t.Fatalf("attr.Autoclass.Enabled = %v, want %v", got, want) + t.Errorf("attr.Autoclass.Enabled = %v, want %v", got, want) } latestToggleTime := attrs.Autoclass.ToggleTime if latestToggleTime.IsZero() { - t.Fatal("got a zero time value, want a populated value") + t.Error("got a zero time value, want a populated value") } if latestToggleTime.Before(toggleTime) { - t.Fatal("latestToggleTime should be newer than bucket creation toggleTime") + t.Error("latestToggleTime should be newer than bucket creation toggleTime") } } From 7e9c34fe26ca4ed52dc04c4b8865778539a2705e Mon Sep 17 00:00:00 2001 From: Cathy Ouyang Date: Tue, 18 Oct 2022 14:36:45 -0700 Subject: [PATCH 8/9] update test --- storage/bucket_test.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/storage/bucket_test.go b/storage/bucket_test.go index f7557ce4135..a1d5ad78d10 100644 --- a/storage/bucket_test.go +++ b/storage/bucket_test.go @@ -802,11 +802,6 @@ func TestNewBucketFromProto(t *testing.T) { }, }}, }, - // Populated with default values. - CustomPlacementConfig: nil, - VersioningEnabled: false, - RequesterPays: false, - ProjectNumber: 0, } got := newBucketFromProto(pb) if diff := testutil.Diff(got, want); diff != "" { From 1819728fc6063e2e1c00430613e9bbd0ad17fec2 Mon Sep 17 00:00:00 2001 From: Cathy Ouyang Date: Wed, 26 Oct 2022 15:25:34 -0700 Subject: [PATCH 9/9] update using multiTransportTest --- storage/grpc_client.go | 3 ++ storage/integration_test.go | 63 ++++++++++++++++++------------------- 2 files changed, 34 insertions(+), 32 deletions(-) diff --git a/storage/grpc_client.go b/storage/grpc_client.go index 00a22fc44c4..8475d384b52 100644 --- a/storage/grpc_client.go +++ b/storage/grpc_client.go @@ -346,6 +346,9 @@ func (c *grpcStorageClient) UpdateBucket(ctx context.Context, bucket string, uat if uattrs.RPO != RPOUnknown { fieldMask.Paths = append(fieldMask.Paths, "rpo") } + if uattrs.Autoclass != nil { + fieldMask.Paths = append(fieldMask.Paths, "autoclass") + } // TODO(cathyo): Handle labels. Pending b/230510191. req.UpdateMask = fieldMask diff --git a/storage/integration_test.go b/storage/integration_test.go index 03f70ff0472..1c7dc84b6b3 100644 --- a/storage/integration_test.go +++ b/storage/integration_test.go @@ -856,44 +856,43 @@ func TestIntegration_PublicAccessPrevention(t *testing.T) { } func TestIntegration_Autoclass(t *testing.T) { - ctx := context.Background() - client := testConfig(ctx, t) - defer client.Close() - h := testHelper{t} + multiTransportTest(context.Background(), t, func(t *testing.T, ctx context.Context, _ string, prefix string, client *Client) { + h := testHelper{t} - // Create a bucket with Autoclass enabled. - bkt := client.Bucket(uidSpace.New()) - h.mustCreate(bkt, testutil.ProjID(), &BucketAttrs{Autoclass: &Autoclass{Enabled: true}}) - defer h.mustDeleteBucket(bkt) + // Create a bucket with Autoclass enabled. + bkt := client.Bucket(prefix + uidSpace.New()) + h.mustCreate(bkt, testutil.ProjID(), &BucketAttrs{Autoclass: &Autoclass{Enabled: true}}) + defer h.mustDeleteBucket(bkt) - // Get Autoclass configuration from bucket attrs. - attrs, err := bkt.Attrs(ctx) - if err != nil { - t.Fatalf("get bucket attrs failed: %v", err) - } - var toggleTime time.Time - if attrs != nil && attrs.Autoclass != nil { - if got, want := attrs.Autoclass.Enabled, true; got != want { + // Get Autoclass configuration from bucket attrs. + attrs, err := bkt.Attrs(ctx) + if err != nil { + t.Fatalf("get bucket attrs failed: %v", err) + } + var toggleTime time.Time + if attrs != nil && attrs.Autoclass != nil { + if got, want := attrs.Autoclass.Enabled, true; got != want { + t.Errorf("attr.Autoclass.Enabled = %v, want %v", got, want) + } + if toggleTime = attrs.Autoclass.ToggleTime; toggleTime.IsZero() { + t.Error("got a zero time value, want a populated value") + } + } + + // Disable Autoclass on the bucket. + ua := BucketAttrsToUpdate{Autoclass: &Autoclass{Enabled: false}} + attrs = h.mustUpdateBucket(bkt, ua, attrs.MetaGeneration) + if got, want := attrs.Autoclass.Enabled, false; got != want { t.Errorf("attr.Autoclass.Enabled = %v, want %v", got, want) } - if toggleTime = attrs.Autoclass.ToggleTime; toggleTime.IsZero() { + latestToggleTime := attrs.Autoclass.ToggleTime + if latestToggleTime.IsZero() { t.Error("got a zero time value, want a populated value") } - } - - // Disable Autoclass on the bucket. - ua := BucketAttrsToUpdate{Autoclass: &Autoclass{Enabled: false}} - attrs = h.mustUpdateBucket(bkt, ua, attrs.MetaGeneration) - if got, want := attrs.Autoclass.Enabled, false; got != want { - t.Errorf("attr.Autoclass.Enabled = %v, want %v", got, want) - } - latestToggleTime := attrs.Autoclass.ToggleTime - if latestToggleTime.IsZero() { - t.Error("got a zero time value, want a populated value") - } - if latestToggleTime.Before(toggleTime) { - t.Error("latestToggleTime should be newer than bucket creation toggleTime") - } + if latestToggleTime.Before(toggleTime) { + t.Error("latestToggleTime should be newer than bucket creation toggleTime") + } + }) } func TestIntegration_ConditionalDelete(t *testing.T) {