Skip to content

Commit 0a22519

Browse files
authored
Merge pull request #81 from IvanOfThings/78-bug-remote-privilege-causes-infinite-state-drift-and-syntax-error
78 bug remote privilege causes infinite state drift and syntax error
2 parents 7a4f033 + ac57cda commit 0a22519

12 files changed

Lines changed: 2977 additions & 117 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,5 @@ website/vendor
3636
.env
3737
**/.terraform.lock.hcl
3838

39-
/examples/resources/schema_migrations/**
39+
/examples/resources/schema_migrations/**
40+
plans/fix-docs-structure-for-registry.md

CLAUDE.md

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
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

Comments
 (0)