Skip to content

Commit 167045c

Browse files
committed
add cloudsql and bigtable syncing
1 parent ee8d22a commit 167045c

File tree

6 files changed

+640
-11
lines changed

6 files changed

+640
-11
lines changed

cmd/ctrlc/root/sync/clickhouse/clickhouse.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ func NewSyncClickhouseCmd() *cobra.Command {
266266
// Create a sanitized name
267267
name := strings.Split(service.Name, ".")[0]
268268
resources = append(resources, api.AgentResource{
269-
Version: "https://schema.ctrlplane.dev/database/v1",
269+
Version: "ctrlplane.dev/database/v1",
270270
Kind: "ClickhouseCloud",
271271
Name: name,
272272
Identifier: fmt.Sprintf("%s/%s", organizationID, service.ID),
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
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

Comments
 (0)