|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
| 4 | + |
| 5 | +## Project Overview |
| 6 | + |
| 7 | +This is a Terraform Provider for ClickHouse database management, built on Terraform Plugin SDK v2. It manages databases, tables, roles, and users in ClickHouse clusters via the native protocol (port 9000). |
| 8 | + |
| 9 | +**Tech Stack:** |
| 10 | +- Go 1.19+ with Terraform Plugin SDK v2 |
| 11 | +- ClickHouse Go driver v2 (github.com/ClickHouse/clickhouse-go/v2) |
| 12 | +- Native ClickHouse protocol (TCP port 9000, not HTTP 8123) |
| 13 | + |
| 14 | +## Development Commands |
| 15 | + |
| 16 | +### Testing |
| 17 | + |
| 18 | +```bash |
| 19 | +# Run all acceptance tests (requires ClickHouse running locally) |
| 20 | +make testacc |
| 21 | + |
| 22 | +# Run acceptance tests with custom environment |
| 23 | +TF_ACC=1 TF_CLICKHOUSE_HOST="127.0.0.1" TF_CLICKHOUSE_USERNAME="default" TF_CLICKHOUSE_PASSWORD="" TF_CLICKHOUSE_PORT=9000 go test ./... -v -timeout 120m |
| 24 | + |
| 25 | +# Run single test |
| 26 | +TF_ACC=1 TF_CLICKHOUSE_HOST=127.0.0.1 TF_CLICKHOUSE_PORT=9000 TF_CLICKHOUSE_USERNAME=default TF_CLICKHOUSE_PASSWORD="" go test -v -run "TestAccResourceRole_AddPrivilegesToEmptyRole" ./pkg/resources/role/... -timeout 30m |
| 27 | + |
| 28 | +# Run unit tests (parallel, no ClickHouse needed) |
| 29 | +make test |
| 30 | +``` |
| 31 | + |
| 32 | +**Important:** `TF_ACC=1` environment variable is REQUIRED to run acceptance tests. Without it, tests are skipped. |
| 33 | + |
| 34 | +### Building |
| 35 | + |
| 36 | +```bash |
| 37 | +# Build binary |
| 38 | +make build |
| 39 | + |
| 40 | +# Install locally for Terraform development |
| 41 | +make install # Linux |
| 42 | +make install-darwin # macOS |
| 43 | + |
| 44 | +# Generate documentation (auto-updates docs/ from code annotations) |
| 45 | +make doc |
| 46 | +# OR |
| 47 | +go generate ./... |
| 48 | +``` |
| 49 | + |
| 50 | +### Local Development Setup |
| 51 | + |
| 52 | +The provider expects ClickHouse running on `127.0.0.1:9000` for tests. Use Docker: |
| 53 | + |
| 54 | +```bash |
| 55 | +docker run -d -p 9000:9000 -p 8123:8123 clickhouse/clickhouse-server |
| 56 | +``` |
| 57 | + |
| 58 | +## Architecture |
| 59 | + |
| 60 | +### Resource Structure Pattern |
| 61 | + |
| 62 | +Every resource follows this consistent pattern: |
| 63 | + |
| 64 | +``` |
| 65 | +pkg/resources/{resource_name}/ |
| 66 | +├── resource_{name}.go # Terraform schema + CRUD operations |
| 67 | +├── service.go # ClickHouse database operations |
| 68 | +├── model.go # Data structures and transformations |
| 69 | +├── validators.go # Custom validation logic |
| 70 | +├── resource_{name}_acceptance_test.go # Acceptance tests |
| 71 | +└── service_test.go # Unit tests for service layer |
| 72 | +``` |
| 73 | + |
| 74 | +**Example: Role Resource Flow** |
| 75 | + |
| 76 | +1. **resource_role.go**: Defines Terraform schema (name, database, privileges), implements CRUD callbacks |
| 77 | +2. **service.go**: Contains `CHRoleService` with methods like `CreateRole()`, `GetRole()`, `UpdateRole()` |
| 78 | +3. **model.go**: Transforms between ClickHouse model (`CHRole`) and Terraform model (`RoleResource`) |
| 79 | +4. **validators.go**: Custom validation (e.g., `ValidatePrivileges()` separates global vs database-level privileges) |
| 80 | + |
| 81 | +### Provider Configuration |
| 82 | + |
| 83 | +Provider establishes a SINGLE connection to ClickHouse that's shared across all resources: |
| 84 | + |
| 85 | +```go |
| 86 | +// pkg/provider/provider.go |
| 87 | +// Creates connection with native protocol |
| 88 | +conn, err := clickhouse.Open(&clickhouse.Options{ |
| 89 | + Addr: []string{fmt.Sprintf("%s:%d", host, port)}, |
| 90 | + Auth: clickhouse.Auth{...}, |
| 91 | +}) |
| 92 | + |
| 93 | +// Returns ApiClient shared by all resources |
| 94 | +return &common.ApiClient{ |
| 95 | + ClickhouseConnection: &conn, |
| 96 | + DefaultCluster: defaultCluster, |
| 97 | +} |
| 98 | +``` |
| 99 | + |
| 100 | +All resources receive this `ApiClient` via the `meta` parameter in CRUD functions. |
| 101 | + |
| 102 | +### Cluster Support |
| 103 | + |
| 104 | +The provider supports both standalone and clustered ClickHouse: |
| 105 | + |
| 106 | +1. **Provider-level default**: `default_cluster` parameter applies to all resources unless overridden |
| 107 | +2. **Resource-level override**: Each resource can specify its own `cluster` attribute |
| 108 | +3. **ON CLUSTER syntax**: Automatically injected by `common.GetClusterStatement(cluster)` in SQL queries |
| 109 | +4. **Metadata persistence**: Cluster name stored in resource comments as JSON for state tracking |
| 110 | + |
| 111 | +**Altinity Operator Support**: Macros like `'{cluster}'`, `'{installation}'`, `'{replica}'` are supported for Kubernetes deployments. |
| 112 | + |
| 113 | +### Comment Encoding Pattern |
| 114 | + |
| 115 | +Resources store metadata in ClickHouse comments as JSON: |
| 116 | + |
| 117 | +```go |
| 118 | +// pkg/common/utils.go |
| 119 | +comment := common.GetComment("user comment", "cluster_name") |
| 120 | +// Produces: {"comment":"user comment","cluster":"cluster_name"} |
| 121 | + |
| 122 | +// On read: |
| 123 | +userComment, cluster, err := common.UnmarshalComment(storedComment) |
| 124 | +``` |
| 125 | + |
| 126 | +This pattern is used by `clickhouse_db` and `clickhouse_table` to track cluster association across Terraform operations. |
| 127 | + |
| 128 | +### Privilege System (Roles) |
| 129 | + |
| 130 | +ClickHouse has TWO types of privileges that require different SQL syntax: |
| 131 | + |
| 132 | +**Global privileges** (require `ON *.*` syntax): |
| 133 | +- REMOTE, S3, AZURE, HDFS, URL, MYSQL, POSTGRES, MONGO, KAFKA |
| 134 | +- These are "expandable" - ClickHouse expands them (e.g., REMOTE → REMOTE + REMOTE_READ + REMOTE_WRITE) |
| 135 | +- Provider MUST use `SHOW GRANTS` (not `system.grants`) to get original privileges, not expanded forms |
| 136 | + |
| 137 | +**Database-level privileges** (require `ON database.*` syntax): |
| 138 | +- SELECT, INSERT, ALTER, DROP TABLE, etc. |
| 139 | +- May require `GRANT CURRENT GRANTS (...)` wrapper for system database |
| 140 | + |
| 141 | +**Critical Implementation Details:** |
| 142 | + |
| 143 | +```go |
| 144 | +// pkg/resources/role/validators.go |
| 145 | +func IsGlobalPrivilege(privilege string) bool { |
| 146 | + // Check if privilege needs ON *.* syntax |
| 147 | +} |
| 148 | + |
| 149 | +// pkg/resources/role/service.go |
| 150 | +func getGrantQuery(roleName, privileges, database) string { |
| 151 | + // Separates global vs database privileges |
| 152 | + // Generates appropriate GRANT statements |
| 153 | +} |
| 154 | +``` |
| 155 | + |
| 156 | +**Known Bug (documented in tests):** The resource currently supports only ONE database per role. `TestAccResourceRole_ChangeDatabaseAndPrivileges` is skipped with `t.Skip()` documenting the DESIRED behavior (accumulate privileges across multiple databases). This is a future enhancement. |
| 157 | + |
| 158 | +### State Drift Fixes |
| 159 | + |
| 160 | +**Empty Privileges Bug (FIXED):** When a role has no privileges, ClickHouse doesn't store database association. The fix in `resource_role.go:93-96` preserves the database field from Terraform state: |
| 161 | + |
| 162 | +```go |
| 163 | +if roleResource.Database == "" && len(roleResource.Privileges.List()) == 0 { |
| 164 | + roleResource.Database = d.Get("database").(string) |
| 165 | +} |
| 166 | +``` |
| 167 | + |
| 168 | +## Testing Guidelines |
| 169 | + |
| 170 | +### Test-Driven Development (TDD) |
| 171 | + |
| 172 | +This project follows strict TDD methodology (enforced by `/wiz:test-driven-development` skill): |
| 173 | + |
| 174 | +1. **RED**: Write failing test that reproduces the bug/feature |
| 175 | +2. **GREEN**: Write minimal code to make test pass |
| 176 | +3. **REFACTOR**: Clean up while keeping tests green |
| 177 | + |
| 178 | +**Critical Rule:** NEVER fix a bug without first writing a test that reproduces it. |
| 179 | + |
| 180 | +### Acceptance Test Pattern |
| 181 | + |
| 182 | +```go |
| 183 | +func TestAccResourceX(t *testing.T) { |
| 184 | + // Optional: Skip for known limitations |
| 185 | + // t.Skip("reason explaining why skipped and desired behavior") |
| 186 | + |
| 187 | + resource.Test(t, resource.TestCase{ |
| 188 | + Providers: testutils.Provider(), |
| 189 | + CheckDestroy: testAccCheckXResourceDestroy([]string{"resource_name"}), |
| 190 | + Steps: []resource.TestStep{ |
| 191 | + { |
| 192 | + // Step 1: Create resource |
| 193 | + Config: fmt.Sprintf(` |
| 194 | + resource "clickhouse_x" "test" { |
| 195 | + name = "%s" |
| 196 | + // ... |
| 197 | + }`, resourceName), |
| 198 | + Check: resource.ComposeTestCheckFunc( |
| 199 | + resource.TestCheckResourceAttr("clickhouse_x.test", "name", resourceName), |
| 200 | + // Custom checks that query ClickHouse directly |
| 201 | + testAccCheckXExistsInClickHouse(resourceName), |
| 202 | + ), |
| 203 | + }, |
| 204 | + { |
| 205 | + // Step 2: Update resource |
| 206 | + Config: fmt.Sprintf(`...updated config...`), |
| 207 | + Check: resource.ComposeTestCheckFunc(...), |
| 208 | + }, |
| 209 | + { |
| 210 | + // Step 3: Verify no drift |
| 211 | + Config: fmt.Sprintf(`...same as step 2...`), |
| 212 | + ExpectNonEmptyPlan: false, // Should be no changes |
| 213 | + }, |
| 214 | + }, |
| 215 | + }) |
| 216 | +} |
| 217 | +``` |
| 218 | + |
| 219 | +**Helper Functions:** Create custom check functions that query ClickHouse directly to verify state: |
| 220 | + |
| 221 | +```go |
| 222 | +func testAccCheckRoleHasPrivileges(roleName string, expectedPrivileges []string) resource.TestCheckFunc { |
| 223 | + return func(s *terraform.State) error { |
| 224 | + // Connect to ClickHouse |
| 225 | + // Run SHOW GRANTS FOR roleName |
| 226 | + // Verify privileges match expected |
| 227 | + return nil |
| 228 | + } |
| 229 | +} |
| 230 | +``` |
| 231 | + |
| 232 | +### Test Utilities |
| 233 | + |
| 234 | +```go |
| 235 | +// pkg/testutils/testutils.go |
| 236 | + |
| 237 | +// Check set/list attributes (order-independent for sets) |
| 238 | +testutils.CheckStateSetAttr("privileges", "clickhouse_role.test", []string{"SELECT", "INSERT"}) |
| 239 | + |
| 240 | +// Get provider for tests |
| 241 | +testutils.Provider() |
| 242 | + |
| 243 | +// Pre-check for acceptance tests |
| 244 | +testutils.TestAccPreCheck(t) |
| 245 | +``` |
| 246 | + |
| 247 | +## Common Patterns |
| 248 | + |
| 249 | +### ID Format |
| 250 | + |
| 251 | +Resources use `{cluster}:{resource_name}` format for IDs: |
| 252 | + |
| 253 | +```go |
| 254 | +d.SetId(fmt.Sprintf("%s:%s", cluster, name)) |
| 255 | + |
| 256 | +// Extract on read: |
| 257 | +idParts := strings.Split(d.Id(), ":") |
| 258 | +cluster := idParts[0] |
| 259 | +name := idParts[1] |
| 260 | +``` |
| 261 | + |
| 262 | +### Service Layer Pattern |
| 263 | + |
| 264 | +All database operations go through service structs: |
| 265 | + |
| 266 | +```go |
| 267 | +type CHRoleService struct { |
| 268 | + CHConnection *driver.Conn |
| 269 | +} |
| 270 | + |
| 271 | +func (s *CHRoleService) CreateRole(ctx context.Context, name, database string, privileges []string) (*CHRole, error) { |
| 272 | + conn := *s.CHConnection |
| 273 | + err := conn.Exec(ctx, fmt.Sprintf("CREATE ROLE %s", name)) |
| 274 | + // ... |
| 275 | +} |
| 276 | +``` |
| 277 | + |
| 278 | +### Error Wrapping |
| 279 | + |
| 280 | +Always wrap errors with context using `fmt.Errorf` with `%w`: |
| 281 | + |
| 282 | +```go |
| 283 | +if err != nil { |
| 284 | + return diag.FromErr(fmt.Errorf("resource role create: %w", err)) |
| 285 | +} |
| 286 | +``` |
| 287 | + |
| 288 | +## Important Files |
| 289 | + |
| 290 | +- **Provider entry**: `pkg/provider/provider.go` - Registers all resources and data sources |
| 291 | +- **Common utilities**: `pkg/common/utils.go` - Comment encoding, cluster statements, set/list conversions |
| 292 | +- **Test utilities**: `pkg/testutils/testutils.go` - Shared test helpers |
| 293 | +- **Examples**: `examples/` - Terraform configurations for manual testing |
| 294 | + |
| 295 | +## Resources Implemented |
| 296 | + |
| 297 | +| Resource | File Location | Description | |
| 298 | +|----------|---------------|-------------| |
| 299 | +| `clickhouse_db` | `pkg/resources/db/` | Manages databases with comments | |
| 300 | +| `clickhouse_table` | `pkg/resources/table/` | Manages tables (MergeTree, Distributed, etc.) | |
| 301 | +| `clickhouse_role` | `pkg/resources/role/` | Manages roles with privileges | |
| 302 | +| `clickhouse_user` | `pkg/resources/user/` | Manages users with passwords and role assignments | |
| 303 | + |
| 304 | +## Data Sources |
| 305 | + |
| 306 | +| Data Source | File Location | Description | |
| 307 | +|-------------|---------------|-------------| |
| 308 | +| `clickhouse_dbs` | `pkg/datasources/` | Lists all databases | |
| 309 | + |
| 310 | +## Known Limitations |
| 311 | + |
| 312 | +1. **Roles**: Currently support only ONE database per role (see skipped test `TestAccResourceRole_ChangeDatabaseAndPrivileges`) |
| 313 | +2. **Table Engines**: Limited replicated table engine support (documented in README) |
| 314 | +3. **Expandable Privileges**: Provider must use `SHOW GRANTS` not `system.grants` to avoid privilege expansion issues |
0 commit comments