Skip to content

Commit 0bea4ba

Browse files
authored
feat(bigtable): wrap admin client (#14534)
Design: [go/cbt-admin-modernization-go](http://goto.google.com/cbt-admin-modernization-go)
1 parent d7b7078 commit 0bea4ba

6 files changed

Lines changed: 683 additions & 18 deletions

File tree

‎bigtable/admin.go‎

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"strings"
2727
"time"
2828

29+
admin "cloud.google.com/go/bigtable/admin/apiv2"
2930
btapb "cloud.google.com/go/bigtable/admin/apiv2/adminpb"
3031
btopt "cloud.google.com/go/bigtable/internal/option"
3132
"cloud.google.com/go/iam"
@@ -101,9 +102,10 @@ func (e ErrPartiallyUnavailable) Error() string {
101102

102103
// AdminClient is a client type for performing admin operations within a specific instance.
103104
type AdminClient struct {
104-
connPool gtransport.ConnPool
105-
tClient btapb.BigtableTableAdminClient
106-
lroClient *lroauto.OperationsClient
105+
connPool gtransport.ConnPool
106+
tClient btapb.BigtableTableAdminClient
107+
lroClient *lroauto.OperationsClient
108+
tableAdminClient *admin.BigtableTableAdminClient
107109

108110
project, instance string
109111

@@ -140,13 +142,19 @@ func NewAdminClient(ctx context.Context, project, instance string, opts ...optio
140142
return nil, err
141143
}
142144

145+
tableAdminClient, err := admin.NewBigtableTableAdminClient(ctx, gtransport.WithConnPool(connPool))
146+
if err != nil {
147+
return nil, err
148+
}
149+
143150
return &AdminClient{
144-
connPool: connPool,
145-
tClient: btapb.NewBigtableTableAdminClient(connPool),
146-
lroClient: lroClient,
147-
project: project,
148-
instance: instance,
149-
md: metadata.Pairs(resourcePrefixHeader, fmt.Sprintf("projects/%s/instances/%s", project, instance)),
151+
connPool: connPool,
152+
tClient: btapb.NewBigtableTableAdminClient(connPool),
153+
lroClient: lroClient,
154+
tableAdminClient: tableAdminClient,
155+
project: project,
156+
instance: instance,
157+
md: metadata.Pairs(resourcePrefixHeader, fmt.Sprintf("projects/%s/instances/%s", project, instance)),
150158
}, nil
151159
}
152160

@@ -155,6 +163,16 @@ func (ac *AdminClient) Close() error {
155163
return ac.connPool.Close()
156164
}
157165

166+
// TableAdminClientV2 returns the GAPIC generated BigtableTableAdminClient.
167+
//
168+
// The returned client shares the underlying connection pool with AdminClient.
169+
// Since the connection pool is shared, calling Close on either the returned client
170+
// or the parent AdminClient will close the connection pool, making both clients
171+
// unusable.
172+
func (ac *AdminClient) TableAdminClientV2() *admin.BigtableTableAdminClient {
173+
return ac.tableAdminClient
174+
}
175+
158176
func (ac *AdminClient) instancePrefix() string {
159177
return instancePrefix(ac.project, ac.instance)
160178
}
@@ -1278,9 +1296,10 @@ const mtlsInstanceAdminAddr = "bigtableadmin.mtls.googleapis.com:443"
12781296
// InstanceAdminClient is a client type for performing admin operations on instances.
12791297
// These operations can be substantially more dangerous than those provided by AdminClient.
12801298
type InstanceAdminClient struct {
1281-
connPool gtransport.ConnPool
1282-
iClient btapb.BigtableInstanceAdminClient
1283-
lroClient *lroauto.OperationsClient
1299+
connPool gtransport.ConnPool
1300+
iClient btapb.BigtableInstanceAdminClient
1301+
lroClient *lroauto.OperationsClient
1302+
instanceAdminClient *admin.BigtableInstanceAdminClient
12841303

12851304
project string
12861305

@@ -1313,13 +1332,18 @@ func NewInstanceAdminClient(ctx context.Context, project string, opts ...option.
13131332
return nil, err
13141333
}
13151334

1316-
return &InstanceAdminClient{
1317-
connPool: connPool,
1318-
iClient: btapb.NewBigtableInstanceAdminClient(connPool),
1319-
lroClient: lroClient,
1335+
instanceAdminClient, err := admin.NewBigtableInstanceAdminClient(ctx, gtransport.WithConnPool(connPool))
1336+
if err != nil {
1337+
return nil, err
1338+
}
13201339

1321-
project: project,
1322-
md: metadata.Pairs(resourcePrefixHeader, "projects/"+project),
1340+
return &InstanceAdminClient{
1341+
connPool: connPool,
1342+
iClient: btapb.NewBigtableInstanceAdminClient(connPool),
1343+
lroClient: lroClient,
1344+
instanceAdminClient: instanceAdminClient,
1345+
project: project,
1346+
md: metadata.Pairs(resourcePrefixHeader, "projects/"+project),
13231347
}, nil
13241348
}
13251349

@@ -1328,6 +1352,16 @@ func (iac *InstanceAdminClient) Close() error {
13281352
return iac.connPool.Close()
13291353
}
13301354

1355+
// InstanceAdminClientV2 returns the GAPIC generated BigtableInstanceAdminClient.
1356+
//
1357+
// The returned client shares the underlying connection pool with InstanceAdminClient.
1358+
// Since the connection pool is shared, calling Close on either the returned client
1359+
// or the parent InstanceAdminClient will close the connection pool, making both clients
1360+
// unusable.
1361+
func (iac *InstanceAdminClient) InstanceAdminClientV2() *admin.BigtableInstanceAdminClient {
1362+
return iac.instanceAdminClient
1363+
}
1364+
13311365
// StorageType is the type of storage used for all tables in an instance
13321366
type StorageType int
13331367

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package admin
16+
17+
import (
18+
"context"
19+
"time"
20+
21+
adminpb "cloud.google.com/go/bigtable/admin/apiv2/adminpb"
22+
gax "github.com/googleapis/gax-go/v2"
23+
)
24+
25+
// RestoreTable creates a new table by restoring from a backup.
26+
func (c *BigtableTableAdminClient) RestoreTable(ctx context.Context, req *adminpb.RestoreTableRequest, opts ...gax.CallOption) error {
27+
op, err := c.restoreTable(ctx, req, opts...)
28+
if err != nil {
29+
return err
30+
}
31+
32+
// Poll the LRO until the table is usable
33+
if _, err := op.Wait(ctx, opts...); err != nil {
34+
return err
35+
}
36+
37+
return nil
38+
}
39+
40+
// WaitForConsistency waits until all the writes committed before the call started have been propagated to all the clusters in the instance via replication.
41+
func (c *BigtableTableAdminClient) WaitForConsistency(ctx context.Context, tableName string, opts ...gax.CallOption) error {
42+
// Get the token.
43+
tokenResp, err := c.GenerateConsistencyToken(ctx, &adminpb.GenerateConsistencyTokenRequest{
44+
Name: tableName,
45+
}, opts...)
46+
if err != nil {
47+
return err
48+
}
49+
token := tokenResp.GetConsistencyToken()
50+
51+
// Periodically check if the token is consistent.
52+
timer := time.NewTicker(time.Second * 10)
53+
defer timer.Stop()
54+
for {
55+
consistentResp, err := c.CheckConsistency(ctx, &adminpb.CheckConsistencyRequest{
56+
Name: tableName,
57+
ConsistencyToken: token,
58+
}, opts...)
59+
if err != nil {
60+
return err
61+
}
62+
if consistentResp.GetConsistent() {
63+
return nil
64+
}
65+
// Sleep for a bit or until the ctx is cancelled.
66+
select {
67+
case <-ctx.Done():
68+
return ctx.Err()
69+
case <-timer.C:
70+
}
71+
}
72+
}
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package admin
16+
17+
import (
18+
"context"
19+
"fmt"
20+
"os"
21+
"testing"
22+
"time"
23+
24+
adminpb "cloud.google.com/go/bigtable/admin/apiv2/adminpb"
25+
"google.golang.org/api/option"
26+
"google.golang.org/grpc"
27+
"google.golang.org/grpc/codes"
28+
"google.golang.org/grpc/credentials/insecure"
29+
"google.golang.org/grpc/status"
30+
"google.golang.org/protobuf/types/known/timestamppb"
31+
)
32+
33+
type testEnv struct {
34+
client *BigtableTableAdminClient
35+
project string
36+
instance string
37+
cluster string
38+
}
39+
40+
func setupIntegration(t *testing.T) *testEnv {
41+
t.Helper()
42+
if testing.Short() {
43+
t.Skip("Skipping integration test in short mode")
44+
}
45+
46+
ctx := context.Background()
47+
emulatorHost := os.Getenv("BIGTABLE_EMULATOR_HOST")
48+
var opts []option.ClientOption
49+
project := os.Getenv("GCLOUD_TESTS_GOLANG_PROJECT_ID")
50+
instance := os.Getenv("GCLOUD_TESTS_BIGTABLE_INSTANCE")
51+
cluster := os.Getenv("GCLOUD_TESTS_BIGTABLE_CLUSTER")
52+
53+
if emulatorHost != "" {
54+
t.Logf("Using emulator at %s", emulatorHost)
55+
opts = append(opts,
56+
option.WithEndpoint(emulatorHost),
57+
option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())),
58+
)
59+
if project == "" {
60+
project = "test-project"
61+
}
62+
if instance == "" {
63+
instance = "test-instance"
64+
}
65+
if cluster == "" {
66+
cluster = "test-cluster"
67+
}
68+
} else if project == "" || instance == "" {
69+
t.Skip("Missing GCLOUD_TESTS_GOLANG_PROJECT_ID or GCLOUD_TESTS_BIGTABLE_INSTANCE for non-emulator run")
70+
}
71+
72+
client, err := NewBigtableTableAdminClient(ctx, opts...)
73+
if err != nil {
74+
t.Fatalf("Failed to create client: %v", err)
75+
}
76+
t.Cleanup(func() {
77+
client.Close()
78+
})
79+
80+
return &testEnv{
81+
client: client,
82+
project: project,
83+
instance: instance,
84+
cluster: cluster,
85+
}
86+
}
87+
88+
func TestIntegration_RestoreTable(t *testing.T) {
89+
env := setupIntegration(t)
90+
if os.Getenv("BIGTABLE_EMULATOR_HOST") == "" && env.cluster == "" {
91+
t.Skip("Missing GCLOUD_TESTS_BIGTABLE_CLUSTER for non-emulator run")
92+
}
93+
94+
ctx := context.Background()
95+
client := env.client
96+
97+
suffix := time.Now().Format("20060102-150405")
98+
sourceTableID := fmt.Sprintf("src-table-%s", suffix)
99+
backupID := fmt.Sprintf("backup-%s", suffix)
100+
restoredTableID := fmt.Sprintf("restored-table-%s", suffix)
101+
102+
instancePath := fmt.Sprintf("projects/%s/instances/%s", env.project, env.instance)
103+
sourceTablePath := fmt.Sprintf("%s/tables/%s", instancePath, sourceTableID)
104+
clusterPath := fmt.Sprintf("%s/clusters/%s", instancePath, env.cluster)
105+
backupPath := fmt.Sprintf("%s/backups/%s", clusterPath, backupID)
106+
restoredTablePath := fmt.Sprintf("%s/tables/%s", instancePath, restoredTableID)
107+
108+
// 1. Create source table
109+
_, err := client.CreateTable(ctx, &adminpb.CreateTableRequest{
110+
Parent: instancePath,
111+
TableId: sourceTableID,
112+
Table: &adminpb.Table{},
113+
})
114+
if err != nil {
115+
t.Fatalf("Failed to create source table: %v", err)
116+
}
117+
t.Cleanup(func() {
118+
client.DeleteTable(ctx, &adminpb.DeleteTableRequest{Name: sourceTablePath})
119+
})
120+
121+
// 2. Create backup
122+
expireTime := time.Now().Add(7 * time.Hour)
123+
opCreateBackup, err := client.CreateBackup(ctx, &adminpb.CreateBackupRequest{
124+
Parent: clusterPath,
125+
BackupId: backupID,
126+
Backup: &adminpb.Backup{
127+
SourceTable: sourceTablePath,
128+
ExpireTime: timestamppb.New(expireTime),
129+
},
130+
})
131+
if err != nil {
132+
st, ok := status.FromError(err)
133+
if ok && st.Code() == codes.Unimplemented {
134+
t.Skip("Emulator does not support CreateBackup")
135+
}
136+
t.Fatalf("Failed to initiate backup: %v", err)
137+
}
138+
t.Cleanup(func() {
139+
client.DeleteBackup(ctx, &adminpb.DeleteBackupRequest{Name: backupPath})
140+
})
141+
142+
_, err = opCreateBackup.Wait(ctx)
143+
if err != nil {
144+
t.Fatalf("Backup LRO failed: %v", err)
145+
}
146+
147+
// 3. Restore table
148+
err = client.RestoreTable(ctx, &adminpb.RestoreTableRequest{
149+
Parent: instancePath,
150+
TableId: restoredTableID,
151+
Source: &adminpb.RestoreTableRequest_Backup{
152+
Backup: backupPath,
153+
},
154+
})
155+
if err != nil {
156+
t.Fatalf("RestoreTable failed: %v", err)
157+
}
158+
t.Cleanup(func() {
159+
client.DeleteTable(ctx, &adminpb.DeleteTableRequest{Name: restoredTablePath})
160+
})
161+
162+
// 4. Verify restored table exists
163+
restoredTable, err := client.GetTable(ctx, &adminpb.GetTableRequest{
164+
Name: restoredTablePath,
165+
})
166+
if err != nil {
167+
t.Fatalf("Failed to get restored table: %v", err)
168+
}
169+
170+
if restoredTable.Name != restoredTablePath {
171+
t.Errorf("Expected restored table name %q, got %q", restoredTablePath, restoredTable.Name)
172+
}
173+
}
174+
175+
func TestIntegration_WaitForConsistency(t *testing.T) {
176+
env := setupIntegration(t)
177+
178+
ctx := context.Background()
179+
client := env.client
180+
181+
suffix := time.Now().Format("20060102-150405")
182+
tableID := fmt.Sprintf("repl-test-table-%s", suffix)
183+
instancePath := fmt.Sprintf("projects/%s/instances/%s", env.project, env.instance)
184+
tablePath := fmt.Sprintf("%s/tables/%s", instancePath, tableID)
185+
186+
// Create table
187+
_, err := client.CreateTable(ctx, &adminpb.CreateTableRequest{
188+
Parent: instancePath,
189+
TableId: tableID,
190+
Table: &adminpb.Table{},
191+
})
192+
if err != nil {
193+
t.Fatalf("Failed to create table: %v", err)
194+
}
195+
t.Cleanup(func() {
196+
client.DeleteTable(ctx, &adminpb.DeleteTableRequest{Name: tablePath})
197+
})
198+
199+
// Wait for replication
200+
err = client.WaitForConsistency(ctx, tablePath)
201+
if err != nil {
202+
t.Fatalf("WaitForConsistency failed: %v", err)
203+
}
204+
}

0 commit comments

Comments
 (0)