From f7c7f41e4d7fcffe05860e1114cb20f40c869da8 Mon Sep 17 00:00:00 2001 From: cojenco Date: Tue, 1 Nov 2022 15:13:09 -0700 Subject: [PATCH] feat(storage): add Autoclass support (#6828) --- storage/bucket.go | 81 +++++++++ storage/bucket_test.go | 332 ++++++++++++++++++++++++++++++++++++ storage/grpc_client.go | 3 + storage/integration_test.go | 40 +++++ 4 files changed, 456 insertions(+) diff --git a/storage/bucket.go b/storage/bucket.go index 52749038640..28a73b8d995 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. + Autoclass *Autoclass } // BucketPolicyOnly is an alias for UniformBucketLevelAccess. @@ -710,6 +715,20 @@ 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 +// 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. + 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 +763,7 @@ func newBucket(b *raw.Bucket) (*BucketAttrs, error) { ProjectNumber: b.ProjectNumber, RPO: toRPO(b), CustomPlacementConfig: customPlacementFromRaw(b.CustomPlacementConfig), + Autoclass: toAutoclassFromRaw(b.Autoclass), }, nil } @@ -776,6 +796,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 +851,7 @@ func (b *BucketAttrs) toRawBucket() *raw.Bucket { IamConfiguration: bktIAM, Rpo: b.RPO.String(), CustomPlacementConfig: b.CustomPlacementConfig.toRawCustomPlacement(), + Autoclass: b.Autoclass.toRawAutoclass(), } } @@ -889,6 +911,7 @@ func (b *BucketAttrs) toProtoBucket() *storagepb.Bucket { IamConfig: bktIAM, Rpo: b.RPO.String(), CustomPlacementConfig: b.CustomPlacementConfig.toProtoCustomPlacement(), + Autoclass: b.Autoclass.toProtoAutoclass(), } } @@ -971,6 +994,7 @@ func (ua *BucketAttrsToUpdate) toProtoBucket() *storagepb.Bucket { Website: ua.Website.toProtoBucketWebsite(), IamConfig: bktIAM, Rpo: ua.RPO.String(), + Autoclass: ua.Autoclass.toProtoAutoclass(), } } @@ -1086,6 +1110,10 @@ type BucketAttrsToUpdate struct { // more information. RPO RPO + // If set, updates the autoclass configuration of the bucket. + // 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. // It is unexported and only used internally by the gRPC client. // Library users should use ACLHandle methods directly. @@ -1199,6 +1227,12 @@ func (ua *BucketAttrsToUpdate) toRawBucket() *raw.Bucket { rb.Website = ua.Website.toRawBucketWebsite() } } + if ua.Autoclass != nil { + rb.Autoclass = &raw.BucketAutoclass{ + Enabled: ua.Autoclass.Enabled, + ForceSendFields: []string{"Enabled"}, + } + } if ua.PredefinedACL != "" { // Clear ACL or the call will fail. rb.Acl = nil @@ -1899,6 +1933,53 @@ func customPlacementFromProto(c *storagepb.Bucket_CustomPlacementConfig) *Custom return &CustomPlacementConfig{DataLocations: c.GetDataLocations()} } +func (a *Autoclass) toRawAutoclass() *raw.BucketAutoclass { + if a == nil { + return nil + } + // Excluding read only field ToggleTime. + return &raw.BucketAutoclass{ + Enabled: a.Enabled, + } +} + +func (a *Autoclass) toProtoAutoclass() *storagepb.Bucket_Autoclass { + if a == nil { + return nil + } + // Excluding read only field ToggleTime. + return &storagepb.Bucket_Autoclass{ + Enabled: a.Enabled, + } +} + +func toAutoclassFromRaw(a *raw.BucketAutoclass) *Autoclass { + if a == nil || a.ToggleTime == "" { + return nil + } + // Return Autoclass.ToggleTime only if parsed with a valid value. + 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 || a.GetToggleTime().AsTime().Unix() == 0 { + 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. diff --git a/storage/bucket_test.go b/storage/bucket_test.go index 7a53625d5f0..a1d5ad78d10 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) { @@ -60,6 +62,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 +166,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 +395,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 +439,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{"Enabled"}}, ForceSendFields: []string{"DefaultEventBasedHold", "Lifecycle"}, } if msg := testutil.Diff(got, want); msg != "" { @@ -638,6 +644,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 +698,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 { @@ -698,6 +712,324 @@ 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", + LocationType: "region", + 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, + LockTime: toProtoTimestamp(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)), + }, + PublicAccessPrevention: "enforced", + }, + Rpo: rpoAsyncTurbo, + Metageneration: int64(39), + CreateTime: toProtoTimestamp(time.Date(2020, 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(2020, 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", + LocationType: "region", + StorageClass: "class", + RetentionPolicy: &RetentionPolicy{ + RetentionPeriod: 3 * time.Second, + EffectiveTime: time.Date(2020, 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(2020, 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(2020, 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"}}}, + Location: "loc", + StorageClass: "class", + RetentionPolicy: &RetentionPolicy{ + RetentionPeriod: 3 * time.Second, + }, + BucketPolicyOnly: BucketPolicyOnly{Enabled: true}, + UniformBucketLevelAccess: UniformBucketLevelAccess{Enabled: true}, + PublicAccessPrevention: PublicAccessPreventionEnforced, + VersioningEnabled: false, + RPO: RPOAsyncTurbo, + 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, + }, + }}, + }, + // 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"}, + }, + 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", + }, + 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 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 93e0e0af027..bd5429c5d1d 100644 --- a/storage/integration_test.go +++ b/storage/integration_test.go @@ -854,6 +854,46 @@ func TestIntegration_PublicAccessPrevention(t *testing.T) { }) } +func TestIntegration_Autoclass(t *testing.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(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 { + 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) + } + 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") + } + }) +} + 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}