Skip to content

Commit 5cd5763

Browse files
committed
feat(cli): configure anon-rules in branchd.json
1 parent 41fea0e commit 5cd5763

File tree

7 files changed

+214
-1
lines changed

7 files changed

+214
-1
lines changed

internal/cli/client/client.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,3 +269,57 @@ func (c *Client) UpdateServer(serverIP string) error {
269269
return nil
270270
}
271271

272+
// AnonRule represents an anonymization rule
273+
type AnonRule struct {
274+
Table string `json:"table"`
275+
Column string `json:"column"`
276+
Template string `json:"template"`
277+
}
278+
279+
// UpdateAnonRulesRequest represents the bulk update request
280+
type UpdateAnonRulesRequest struct {
281+
Rules []AnonRule `json:"rules"`
282+
}
283+
284+
// UpdateAnonRules bulk replaces all anonymization rules
285+
func (c *Client) UpdateAnonRules(serverIP string, rules []AnonRule) error {
286+
token, err := auth.LoadToken(serverIP)
287+
if err != nil {
288+
return err
289+
}
290+
291+
reqBody := UpdateAnonRulesRequest{
292+
Rules: rules,
293+
}
294+
295+
jsonData, err := json.Marshal(reqBody)
296+
if err != nil {
297+
return fmt.Errorf("failed to marshal request: %w", err)
298+
}
299+
300+
req, err := http.NewRequest(
301+
"PUT",
302+
fmt.Sprintf("%s/api/anon-rules", c.baseURL),
303+
bytes.NewBuffer(jsonData),
304+
)
305+
if err != nil {
306+
return fmt.Errorf("failed to create request: %w", err)
307+
}
308+
309+
req.Header.Set("Content-Type", "application/json")
310+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
311+
312+
resp, err := c.httpClient.Do(req)
313+
if err != nil {
314+
return fmt.Errorf("failed to send request: %w", err)
315+
}
316+
defer resp.Body.Close()
317+
318+
if resp.StatusCode != http.StatusOK {
319+
body, _ := io.ReadAll(resp.Body)
320+
return fmt.Errorf("failed to update anon rules (status %d): %s", resp.StatusCode, string(body))
321+
}
322+
323+
return nil
324+
}
325+
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package commands
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/branchd-dev/branchd/internal/cli/client"
7+
"github.com/branchd-dev/branchd/internal/cli/config"
8+
"github.com/spf13/cobra"
9+
)
10+
11+
// NewUpdateAnonRulesCmd creates the update-anon-rules command
12+
func NewUpdateAnonRulesCmd() *cobra.Command {
13+
cmd := &cobra.Command{
14+
Use: "update-anon-rules",
15+
Short: "Update anonymization rules on all servers",
16+
RunE: func(cmd *cobra.Command, args []string) error {
17+
return runUpdateAnonRules()
18+
},
19+
}
20+
21+
return cmd
22+
}
23+
24+
func runUpdateAnonRules() error {
25+
// Load config from current directory
26+
cfg, err := config.LoadFromCurrentDir()
27+
if err != nil {
28+
return fmt.Errorf("failed to load config: %w\nRun 'branchd init' to create a configuration file", err)
29+
}
30+
31+
// Check if anonRules are defined
32+
if cfg.AnonRules == nil {
33+
return fmt.Errorf("no anonRules defined in branchd.json")
34+
}
35+
36+
if len(cfg.Servers) == 0 {
37+
return fmt.Errorf("no servers configured. Run 'branchd init' to add a server")
38+
}
39+
40+
// Convert config rules to client rules
41+
var rules []client.AnonRule
42+
for _, rule := range cfg.AnonRules {
43+
rules = append(rules, client.AnonRule{
44+
Table: rule.Table,
45+
Column: rule.Column,
46+
Template: rule.Template,
47+
})
48+
}
49+
50+
// Update all servers
51+
for _, server := range cfg.Servers {
52+
if server.IP == "" {
53+
continue
54+
}
55+
56+
fmt.Printf("Updating server '%s' (%s)... ", server.Alias, server.IP)
57+
58+
// Create API client
59+
apiClient := client.New(server.IP)
60+
61+
// Update rules on server
62+
if err := apiClient.UpdateAnonRules(server.IP, rules); err != nil {
63+
fmt.Printf("Failed to update server '%s': %v\n", server.Alias, err)
64+
continue
65+
}
66+
67+
fmt.Printf("Done\n")
68+
}
69+
70+
return nil
71+
}

internal/cli/config/config.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,17 @@ type Server struct {
1515
Alias string `json:"alias"`
1616
}
1717

18+
// AnonRule represents an anonymization rule
19+
type AnonRule struct {
20+
Table string `json:"table"`
21+
Column string `json:"column"`
22+
Template string `json:"template"`
23+
}
24+
1825
// Config represents the CLI configuration file
1926
type Config struct {
20-
Servers []Server `json:"servers"`
27+
Servers []Server `json:"servers"`
28+
AnonRules []AnonRule `json:"anonRules,omitempty"`
2129
}
2230

2331
// DefaultConfig returns a default configuration with example servers

internal/cli/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ func init() {
4747
rootCmd.AddCommand(commands.NewSelectServerCmd())
4848
rootCmd.AddCommand(commands.NewUpdateCmd(version))
4949
rootCmd.AddCommand(commands.NewUpdateServerCmd())
50+
rootCmd.AddCommand(commands.NewUpdateAnonRulesCmd())
5051
}
5152

5253
// Execute runs the root command

internal/server/anon_rules_handlers.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ type CreateAnonRuleRequest struct {
1515
Template string `json:"template" binding:"required"`
1616
}
1717

18+
type UpdateAnonRulesRequest struct {
19+
Rules []CreateAnonRuleRequest `json:"rules" binding:"required"`
20+
}
21+
1822
// @Router /api/anon-rules [get]
1923
// @Success 200 {object} []models.AnonRule
2024
func (s *Server) listAnonRules(c *gin.Context) {
@@ -93,3 +97,61 @@ func (s *Server) deleteAnonRule(c *gin.Context) {
9397

9498
c.Status(http.StatusNoContent)
9599
}
100+
101+
// @Router /api/anon-rules [put]
102+
// @Param request body UpdateAnonRulesRequest true "Update anon rules request"
103+
// @Success 200 {object} []models.AnonRule
104+
func (s *Server) updateAnonRules(c *gin.Context) {
105+
var req UpdateAnonRulesRequest
106+
if err := c.ShouldBindJSON(&req); err != nil {
107+
s.logger.Warn().Err(err).Msg("Invalid request body")
108+
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()})
109+
return
110+
}
111+
112+
// Use transaction to ensure atomicity (delete all + insert all)
113+
err := s.db.Transaction(func(tx *gorm.DB) error {
114+
// Delete all existing rules
115+
if err := tx.Where("1=1").Delete(&models.AnonRule{}).Error; err != nil {
116+
return err
117+
}
118+
119+
// Insert new rules
120+
var newRules []models.AnonRule
121+
for _, rule := range req.Rules {
122+
newRules = append(newRules, models.AnonRule{
123+
Table: rule.Table,
124+
Column: rule.Column,
125+
Template: rule.Template,
126+
})
127+
}
128+
129+
if len(newRules) > 0 {
130+
if err := tx.Create(&newRules).Error; err != nil {
131+
return err
132+
}
133+
}
134+
135+
return nil
136+
})
137+
138+
if err != nil {
139+
s.logger.Error().Err(err).Msg("Failed to update anon rules")
140+
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update anonymization rules"})
141+
return
142+
}
143+
144+
// Load and return the new rules
145+
var rules []models.AnonRule
146+
if err := s.db.Order("created_at DESC").Find(&rules).Error; err != nil {
147+
s.logger.Error().Err(err).Msg("Failed to load anon rules")
148+
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"})
149+
return
150+
}
151+
152+
s.logger.Info().
153+
Int("count", len(rules)).
154+
Msg("Updated anonymization rules")
155+
156+
c.JSON(http.StatusOK, rules)
157+
}

internal/server/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ func (s *Server) setupRouter() {
265265
// Anonymization rules (global)
266266
api.GET("/anon-rules", s.listAnonRules)
267267
api.POST("/anon-rules", s.createAnonRule)
268+
api.PUT("/anon-rules", s.updateAnonRules)
268269
api.DELETE("/anon-rules/:id", s.deleteAnonRule)
269270

270271
// Branches

web/src/lib/openapi.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,10 @@ export interface InternalServerSystemInfoResponse {
146146
vm?: InternalServerVMMetrics;
147147
}
148148

149+
export interface InternalServerUpdateAnonRulesRequest {
150+
rules: InternalServerCreateAnonRuleRequest[];
151+
}
152+
149153
export interface InternalServerUpdateConfigRequest {
150154
connectionString?: string;
151155
domain?: string;
@@ -433,6 +437,18 @@ export class Api<
433437
...params,
434438
}),
435439

440+
anonRulesUpdate: (
441+
request: InternalServerUpdateAnonRulesRequest,
442+
params: RequestParams = {},
443+
) =>
444+
this.request<GithubComBranchdDevBranchdInternalModelsAnonRule[], any>({
445+
path: `/api/anon-rules`,
446+
method: "PUT",
447+
body: request,
448+
type: ContentType.Json,
449+
...params,
450+
}),
451+
436452
anonRulesCreate: (
437453
request: InternalServerCreateAnonRuleRequest,
438454
params: RequestParams = {},

0 commit comments

Comments
 (0)