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+ }
0 commit comments