1+ package bigtable
2+
3+ import (
4+ "context"
5+ "fmt"
6+ "slices"
7+ "sort"
8+ "strconv"
9+ "strings"
10+
11+ "github.com/MakeNowJust/heredoc/v2"
12+ "github.com/charmbracelet/log"
13+ "github.com/ctrlplanedev/cli/internal/api"
14+ "github.com/spf13/cobra"
15+ "github.com/spf13/viper"
16+ "google.golang.org/api/bigtableadmin/v2"
17+ )
18+
19+ // BigtableInstance represents a Google Cloud Bigtable instance
20+ type BigtableInstance struct {
21+ ID string `json:"id"`
22+ Name string `json:"name"`
23+ ConnectionMethod ConnectionMethod `json:"connectionMethod"`
24+ }
25+
26+ // ConnectionMethod contains connection details for a Bigtable instance
27+ type ConnectionMethod struct {
28+ Type string `json:"type"`
29+ Project string `json:"project"`
30+ Instance string `json:"instance"`
31+ }
32+
33+ // NewSyncBigtableCmd creates a new cobra command for syncing Bigtable instances
34+ func NewSyncBigtableCmd () * cobra.Command {
35+ var project string
36+ var name string
37+
38+ cmd := & cobra.Command {
39+ Use : "google-bigtable" ,
40+ Short : "Sync Google Bigtable instances into Ctrlplane" ,
41+ Example : heredoc .Doc (`
42+ # Make sure Google Cloud credentials are configured via environment variables or application default credentials
43+
44+ # Sync all Bigtable instances from a project
45+ $ ctrlc sync google-bigtable --project my-project
46+ ` ),
47+ PreRunE : validateFlags (& project ),
48+ RunE : runSync (& project , & name ),
49+ }
50+
51+ // Add command flags
52+ cmd .Flags ().StringVarP (& name , "provider" , "p" , "" , "Name of the resource provider" )
53+ cmd .Flags ().StringVarP (& project , "project" , "c" , "" , "Google Cloud Project ID" )
54+ cmd .MarkFlagRequired ("project" )
55+
56+ return cmd
57+ }
58+
59+ // validateFlags ensures required flags are set
60+ func validateFlags (project * string ) func (cmd * cobra.Command , args []string ) error {
61+ return func (cmd * cobra.Command , args []string ) error {
62+ if * project == "" {
63+ return fmt .Errorf ("project is required" )
64+ }
65+ return nil
66+ }
67+ }
68+
69+ // runSync contains the main sync logic
70+ func runSync (project , name * string ) func (cmd * cobra.Command , args []string ) error {
71+ return func (cmd * cobra.Command , args []string ) error {
72+ log .Info ("Syncing Bigtable instances into Ctrlplane" , "project" , * project )
73+
74+ ctx := context .Background ()
75+
76+ // Initialize clients
77+ adminClient , err := initBigtableClient (ctx )
78+ if err != nil {
79+ return err
80+ }
81+
82+ // List and process instances
83+ resources , err := processInstances (ctx , adminClient , * project )
84+ if err != nil {
85+ return err
86+ }
87+
88+ // Upsert resources to Ctrlplane
89+ return upsertToCtrlplane (ctx , resources , project , name )
90+ }
91+ }
92+
93+ // initBigtableClient creates a new Bigtable Admin client
94+ func initBigtableClient (ctx context.Context ) (* bigtableadmin.Service , error ) {
95+ adminClient , err := bigtableadmin .NewService (ctx )
96+ if err != nil {
97+ return nil , fmt .Errorf ("failed to create Bigtable Admin client: %w" , err )
98+ }
99+ return adminClient , nil
100+ }
101+
102+ // processInstances lists and processes all Bigtable instances
103+ func processInstances (ctx context.Context , adminClient * bigtableadmin.Service , project string ) ([]api.AgentResource , error ) {
104+ projectParent := fmt .Sprintf ("projects/%s" , project )
105+ instances , err := adminClient .Projects .Instances .List (projectParent ).Do ()
106+ if err != nil {
107+ return nil , fmt .Errorf ("failed to list instances: %w" , err )
108+ }
109+
110+ log .Info ("Found instances" , "count" , len (instances .Instances ))
111+
112+ resources := []api.AgentResource {}
113+ for _ , instance := range instances .Instances {
114+ resource , err := processInstance (ctx , adminClient , instance , project )
115+ if err != nil {
116+ log .Error ("Failed to process instance" , "name" , instance .Name , "error" , err )
117+ continue
118+ }
119+ resources = append (resources , resource )
120+ }
121+
122+ return resources , nil
123+ }
124+
125+ // processInstance handles processing of a single Bigtable instance
126+ func processInstance (ctx context.Context , adminClient * bigtableadmin.Service , instance * bigtableadmin.Instance , project string ) (api.AgentResource , error ) {
127+ metadata := initInstanceMetadata (instance , project )
128+
129+ // Process clusters
130+ locations , err := processClusters (adminClient , instance , metadata )
131+ if err != nil {
132+ log .Error ("Error processing clusters" , "error" , err )
133+ }
134+ sort .Strings (locations )
135+ metadata ["google/locations" ] = strings .Join (locations , "," )
136+
137+ // Process tables
138+ if err := processTables (adminClient , instance , metadata ); err != nil {
139+ log .Error ("Error processing tables" , "error" , err )
140+ }
141+
142+ // Build console URL and instance identifier
143+ consoleUrl := fmt .Sprintf ("https://console.cloud.google.com/bigtable/instances/%s/overview?project=%s" ,
144+ instance .Name , project )
145+ metadata ["ctrlplane/links" ] = fmt .Sprintf ("{ \" Google Cloud Console\" : \" %s\" }" , consoleUrl )
146+ instanceFullName := fmt .Sprintf ("projects/%s/instances/%s" , project , instance .Name )
147+
148+ return api.AgentResource {
149+ Version : "ctrlplane.dev/database/v1" ,
150+ Kind : "GoogleBigtable" ,
151+ Name : instance .DisplayName ,
152+ Identifier : instanceFullName ,
153+ Config : map [string ]any {
154+ "name" : instance .Name ,
155+ "host" : instance .Name ,
156+ "port" : 443 ,
157+ "googleBigtable" : map [string ]any {
158+ "project" : project ,
159+ "instanceId" : instance .Name ,
160+ "state" : strings .ToLower (instance .State ),
161+ "type" : instance .Type ,
162+ },
163+ },
164+ Metadata : metadata ,
165+ }, nil
166+ }
167+
168+ // initInstanceMetadata initializes the base metadata for an instance
169+ func initInstanceMetadata (instance * bigtableadmin.Instance , project string ) map [string ]string {
170+ consoleUrl := fmt .Sprintf ("https://console.cloud.google.com/bigtable/instances/%s/overview?project=%s" ,
171+ instance .Name , project )
172+
173+ return map [string ]string {
174+ "database/type" : "bigtable" ,
175+ "database/host" : instance .Name ,
176+ "database/port" : "443" ,
177+ "google/project" : project ,
178+ "google/instance-type" : "bigtable" ,
179+ "google/console-url" : consoleUrl ,
180+ "google/state" : strings .ToLower (instance .State ),
181+ "google/type" : instance .Type ,
182+ }
183+ }
184+
185+ // processClusters handles processing of Bigtable clusters
186+ func processClusters (adminClient * bigtableadmin.Service , instance * bigtableadmin.Instance , metadata map [string ]string ) ([]string , error ) {
187+ log .Info ("Listing clusters" , "name" , instance .Name )
188+
189+ clusters , err := adminClient .Projects .Instances .Clusters .List (instance .Name ).Do ()
190+ if err != nil {
191+ return nil , err
192+ }
193+
194+ locations := []string {}
195+ if clusters != nil {
196+ metadata ["google/bigtable/cluster-count" ] = strconv .FormatInt (int64 (len (clusters .Clusters )), 10 )
197+
198+ for _ , cluster := range clusters .Clusters {
199+ name := strings .ReplaceAll (cluster .Name , instance .Name + "/clusters/" , "" )
200+ location := strings .ReplaceAll (cluster .Location , "projects/" + instance .Name + "/locations/" , "" )
201+
202+ metadata [fmt .Sprintf ("google/bigtable/cluster/%s" , name )] = "true"
203+ metadata [fmt .Sprintf ("google/bigtable/cluster/%s/location" , name )] = location
204+ metadata [fmt .Sprintf ("google/bigtable/cluster/%s/state" , name )] = cluster .State
205+ metadata [fmt .Sprintf ("google/bigtable/cluster/%s/serve-nodes" , name )] = strconv .FormatInt (cluster .ServeNodes , 10 )
206+
207+ if ! slices .Contains (locations , location ) {
208+ locations = append (locations , location )
209+ }
210+ }
211+ }
212+
213+ return locations , nil
214+ }
215+
216+ // processTables handles processing of Bigtable tables
217+ func processTables (adminClient * bigtableadmin.Service , instance * bigtableadmin.Instance , metadata map [string ]string ) error {
218+ log .Info ("Listing tables" , "name" , instance .Name )
219+
220+ tables , err := adminClient .Projects .Instances .Tables .List (instance .Name ).Do ()
221+ if err != nil {
222+ return err
223+ }
224+
225+ if tables != nil {
226+ tableNames := []string {}
227+ totalSizeBytes := int64 (0 )
228+
229+ metadata ["google/bigtable/table-count" ] = strconv .FormatInt (int64 (len (tables .Tables )), 10 )
230+
231+ for _ , table := range tables .Tables {
232+ name := strings .ReplaceAll (table .Name , instance .Name + "/tables/" , "" )
233+ tableNames = append (tableNames , name )
234+
235+ metadata [fmt .Sprintf ("google/bigtable/table/%s" , name )] = "true"
236+
237+ if table .Stats != nil {
238+ totalSizeBytes += table .Stats .LogicalDataBytes
239+ metadata [fmt .Sprintf ("google/bigtable/table/%s/row-count" , name )] = strconv .FormatInt (table .Stats .RowCount , 10 )
240+ metadata [fmt .Sprintf ("google/bigtable/table/%s/size-gb" , name )] = strconv .FormatFloat (float64 (table .Stats .LogicalDataBytes )/ 1024 / 1024 / 1024 , 'f' , 2 , 64 )
241+ }
242+ }
243+
244+ sort .Strings (tableNames )
245+ metadata ["google/bigtable/tables" ] = strings .Join (tableNames , "," )
246+ metadata ["database/size-gb" ] = strconv .FormatFloat (float64 (totalSizeBytes )/ 1024 / 1024 / 1024 , 'f' , 0 , 64 )
247+ }
248+
249+ return nil
250+ }
251+
252+ // upsertToCtrlplane handles upserting resources to Ctrlplane
253+ func upsertToCtrlplane (ctx context.Context , resources []api.AgentResource , project , name * string ) error {
254+ if * name == "" {
255+ * name = fmt .Sprintf ("google-bigtable-project-%s" , * project )
256+ }
257+
258+ apiURL := viper .GetString ("url" )
259+ apiKey := viper .GetString ("api-key" )
260+ workspaceId := viper .GetString ("workspace" )
261+
262+ ctrlplaneClient , err := api .NewAPIKeyClientWithResponses (apiURL , apiKey )
263+ if err != nil {
264+ return fmt .Errorf ("failed to create API client: %w" , err )
265+ }
266+
267+ rp , err := api .NewResourceProvider (ctrlplaneClient , workspaceId , * name )
268+ if err != nil {
269+ return fmt .Errorf ("failed to create resource provider: %w" , err )
270+ }
271+
272+ upsertResp , err := rp .UpsertResource (ctx , resources )
273+ if err != nil {
274+ return fmt .Errorf ("failed to upsert resources: %w" , err )
275+ }
276+
277+ log .Info ("Response from upserting resources" , "status" , upsertResp .Status )
278+ return nil
279+ }
0 commit comments