Skip to content

Commit b5d1206

Browse files
committed
add clickhouse console sync
1 parent a3586da commit b5d1206

File tree

2 files changed

+193
-1
lines changed

2 files changed

+193
-1
lines changed
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package clickhouse
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"net/http"
8+
"os"
9+
"strings"
10+
11+
"github.com/MakeNowJust/heredoc/v2"
12+
"github.com/charmbracelet/log"
13+
"github.com/ctrlplanedev/cli/internal/api"
14+
"github.com/ctrlplanedev/cli/internal/cliutil"
15+
"github.com/spf13/cobra"
16+
"github.com/spf13/viper"
17+
)
18+
19+
type ClickHouseConfig struct {
20+
ID string `json:"id"`
21+
Name string `json:"name"`
22+
State string `json:"state"`
23+
Region string `json:"region"`
24+
CloudProvider string `json:"cloudProvider"`
25+
Tier string `json:"tier"`
26+
IdleScaling map[string]interface{} `json:"idleScaling"`
27+
TotalDiskSize int `json:"totalDiskSize"`
28+
TotalMemoryMB int `json:"totalMemoryMB"`
29+
MinTotalMemory int `json:"minTotalMemory"`
30+
MaxTotalMemory int `json:"maxTotalMemory"`
31+
Created string `json:"created"`
32+
Endpoints []map[string]interface{} `json:"endpoints"`
33+
}
34+
35+
func (c *ClickHouseConfig) Struct() map[string]interface{} {
36+
b, _ := json.Marshal(c)
37+
var m map[string]interface{}
38+
json.Unmarshal(b, &m)
39+
return m
40+
}
41+
42+
type ClickHouseClient struct {
43+
httpClient *http.Client
44+
apiUrl string
45+
apiKey string
46+
organizationID string
47+
}
48+
49+
func NewClickHouseClient(apiUrl, apiKey, organizationID string) *ClickHouseClient {
50+
return &ClickHouseClient{
51+
httpClient: &http.Client{},
52+
apiUrl: apiUrl,
53+
apiKey: apiKey,
54+
organizationID: organizationID,
55+
}
56+
}
57+
58+
type ServiceList struct {
59+
Services []Service `json:"services"`
60+
}
61+
62+
type Service struct {
63+
ID string `json:"id"`
64+
Name string `json:"name"`
65+
State string `json:"state"`
66+
Region string `json:"region"`
67+
CloudProvider string `json:"cloudProvider"`
68+
Tier string `json:"tier"`
69+
IdleScaling map[string]interface{} `json:"idleScaling"`
70+
TotalDiskSize int `json:"totalDiskSize"`
71+
TotalMemoryMB int `json:"totalMemoryMB"`
72+
MinTotalMemory int `json:"minTotalMemory"`
73+
MaxTotalMemory int `json:"maxTotalMemory"`
74+
Created string `json:"created"`
75+
Endpoints []map[string]interface{} `json:"endpoints"`
76+
}
77+
78+
func (c *ClickHouseClient) GetServices(ctx context.Context) ([]Service, error) {
79+
url := fmt.Sprintf("%s/v1/organizations/%s/services", c.apiUrl, c.organizationID)
80+
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
81+
if err != nil {
82+
return nil, fmt.Errorf("failed to create request: %w", err)
83+
}
84+
85+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
86+
req.Header.Set("Content-Type", "application/json")
87+
88+
resp, err := c.httpClient.Do(req)
89+
if err != nil {
90+
return nil, fmt.Errorf("failed to make request: %w", err)
91+
}
92+
defer resp.Body.Close()
93+
94+
if resp.StatusCode != http.StatusOK {
95+
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
96+
}
97+
98+
var result ServiceList
99+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
100+
return nil, fmt.Errorf("failed to decode response: %w", err)
101+
}
102+
103+
return result.Services, nil
104+
}
105+
106+
func NewSyncClickhouseCmd() *cobra.Command {
107+
var providerName string
108+
var clickhouseApiUrl string
109+
var clickhouseApiKey string
110+
var organizationID string
111+
112+
cmd := &cobra.Command{
113+
Use: "clickhouse",
114+
Short: "Sync ClickHouse instances into Ctrlplane",
115+
Example: heredoc.Doc(`
116+
$ ctrlc sync clickhouse --workspace 2a7c5560-75c9-4dbe-be74-04ee33bf8188
117+
`),
118+
PreRunE: func(cmd *cobra.Command, args []string) error {
119+
if clickhouseApiKey == "" {
120+
return fmt.Errorf("clickhouse-key must be provided")
121+
}
122+
if organizationID == "" {
123+
return fmt.Errorf("organization-id must be provided")
124+
}
125+
return nil
126+
},
127+
RunE: func(cmd *cobra.Command, args []string) error {
128+
log.Info("Syncing ClickHouse instances into Ctrlplane")
129+
apiURL := viper.GetString("url")
130+
apiKey := viper.GetString("api-key")
131+
workspaceId := viper.GetString("workspace")
132+
ctrlplaneClient, err := api.NewAPIKeyClientWithResponses(apiURL, apiKey)
133+
if err != nil {
134+
return fmt.Errorf("failed to create API client: %w", err)
135+
}
136+
chClient := NewClickHouseClient(clickhouseApiUrl, clickhouseApiKey, organizationID)
137+
ctx := context.Background()
138+
services, err := chClient.GetServices(ctx)
139+
if err != nil {
140+
return fmt.Errorf("failed to list ClickHouse services: %w", err)
141+
}
142+
resources := []api.AgentResource{}
143+
for _, service := range services {
144+
metadata := map[string]string{}
145+
metadata["clickhouse/id"] = service.ID
146+
metadata["clickhouse/name"] = service.Name
147+
metadata["clickhouse/state"] = service.State
148+
metadata["clickhouse/region"] = service.Region
149+
metadata["clickhouse/cloud-provider"] = service.CloudProvider
150+
metadata["clickhouse/tier"] = service.Tier
151+
metadata["clickhouse/created"] = service.Created
152+
153+
config := ClickHouseConfig(service) // Direct type conversion since fields match
154+
155+
// Create a sanitized name
156+
name := strings.Split(service.Name, ".")[0]
157+
resources = append(resources, api.AgentResource{
158+
Version: "clickhouse/v1",
159+
Kind: "Service",
160+
Name: name,
161+
Identifier: fmt.Sprintf("%s/%s", organizationID, service.ID),
162+
Config: config.Struct(),
163+
Metadata: metadata,
164+
})
165+
}
166+
log.Info("Upserting resources", "count", len(resources))
167+
providerName := fmt.Sprintf("clickhouse-%s", organizationID)
168+
rp, err := api.NewResourceProvider(ctrlplaneClient, workspaceId, providerName)
169+
if err != nil {
170+
return fmt.Errorf("failed to create resource provider: %w", err)
171+
}
172+
upsertResp, err := rp.UpsertResource(ctx, resources)
173+
log.Info("Response from upserting resources", "status", upsertResp.Status)
174+
if err != nil {
175+
return fmt.Errorf("failed to upsert resources: %w", err)
176+
}
177+
return cliutil.HandleResponseOutput(cmd, upsertResp)
178+
},
179+
}
180+
181+
cmd.Flags().StringVarP(&providerName, "provider", "p", "clickhouse", "The name of the provider to use")
182+
cmd.Flags().StringVarP(&clickhouseApiUrl, "clickhouse-url", "u", "https://api.clickhouse.cloud", "The URL of the ClickHouse API")
183+
cmd.Flags().StringVarP(&clickhouseApiKey, "clickhouse-key", "k", os.Getenv("CLICKHOUSE_API_KEY"), "The API key to use")
184+
cmd.Flags().StringVarP(&organizationID, "organization-id", "o", os.Getenv("CLICKHOUSE_ORGANIZATION_ID"), "The ClickHouse organization ID")
185+
186+
cmd.MarkFlagRequired("organization-id")
187+
188+
return cmd
189+
}

cmd/ctrlc/root/sync/sync.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package sync
22

33
import (
44
"github.com/MakeNowJust/heredoc/v2"
5+
"github.com/ctrlplanedev/cli/cmd/ctrlc/root/sync/clickhouse"
56
"github.com/ctrlplanedev/cli/cmd/ctrlc/root/sync/tailscale"
67
"github.com/ctrlplanedev/cli/cmd/ctrlc/root/sync/terraform"
78
"github.com/ctrlplanedev/cli/internal/cliutil"
@@ -17,13 +18,15 @@ func NewSyncCmd() *cobra.Command {
1718
Example: heredoc.Doc(`
1819
$ ctrlc sync tfe --interval 5m # Run every 5 minutes
1920
$ ctrlc sync tailscale --interval 1h # Run every hour
21+
$ ctrlc sync clickhouse # Run once
2022
`),
2123
}
2224

2325
cmd.PersistentFlags().StringVar(&interval, "interval", "", "Run commands on an interval (5m, 1h, 1d)")
2426

2527
cmd.AddCommand(cliutil.AddIntervalSupport(terraform.NewSyncTerraformCmd(), ""))
2628
cmd.AddCommand(cliutil.AddIntervalSupport(tailscale.NewSyncTailscaleCmd(), ""))
29+
cmd.AddCommand(cliutil.AddIntervalSupport(clickhouse.NewSyncClickhouseCmd(), ""))
2730

2831
return cmd
29-
}
32+
}

0 commit comments

Comments
 (0)