| 1 | package configport |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "database/sql" |
| 6 | "encoding/hex" |
| 7 | "encoding/json" |
| 8 | "errors" |
| 9 | "fmt" |
| 10 | "sort" |
| 11 | "strings" |
| 12 | "time" |
| 13 | |
| 14 | "github.com/FelineStateMachine/tinyrelay/internal/community" |
| 15 | "github.com/FelineStateMachine/tinyrelay/internal/policy" |
| 16 | "github.com/FelineStateMachine/tinyrelay/internal/replication" |
| 17 | "github.com/FelineStateMachine/tinyrelay/internal/storage" |
| 18 | "github.com/FelineStateMachine/tinyrelay/internal/templates" |
| 19 | ) |
| 20 | |
| 21 | const Format = "bind.ws/relay-config/2" |
| 22 | |
| 23 | // Member is a community member in an exported configuration. |
| 24 | type Member struct { |
| 25 | PubKey string `json:"pubkey"` |
| 26 | Name string `json:"name,omitempty"` |
| 27 | Note string `json:"note,omitempty"` |
| 28 | Role string `json:"role"` |
| 29 | } |
| 30 | |
| 31 | // Rule is a retention rule. A nil Kind applies to the tenant default. |
| 32 | type Rule struct { |
| 33 | Kind *int `json:"kind"` |
| 34 | Days int `json:"days"` |
| 35 | } |
| 36 | |
| 37 | // Config is the JSON representation accepted by Parse and produced by |
| 38 | // Export. Sections records which keys were present in the input so Apply can |
| 39 | // distinguish omission from an explicit empty replacement. |
| 40 | type Config struct { |
| 41 | Format string `json:"format"` |
| 42 | ExportedAt int64 `json:"exported_at,omitempty"` |
| 43 | Name string `json:"name,omitempty"` |
| 44 | Template *templates.Template `json:"template,omitempty"` |
| 45 | Policy map[string]json.RawMessage `json:"policy,omitempty"` |
| 46 | Members []Member `json:"members,omitempty"` |
| 47 | Bans []map[string]any `json:"bans,omitempty"` |
| 48 | Addresses []map[string]any `json:"addresses,omitempty"` |
| 49 | BannedEvents []map[string]any `json:"banned_events,omitempty"` |
| 50 | Kinds struct { |
| 51 | Allow []int `json:"allow"` |
| 52 | Block []int `json:"block"` |
| 53 | } `json:"kinds,omitempty"` |
| 54 | Retention []Rule `json:"retention,omitempty"` |
| 55 | Connections []map[string]any `json:"connections,omitempty"` |
| 56 | Jobs []map[string]any `json:"jobs,omitempty"` |
| 57 | Sections []string `json:"-"` |
| 58 | Warnings []string `json:"-"` |
| 59 | } |
| 60 | |
| 61 | // Changes describes the policy and community sections changed by an apply. |
| 62 | type Changes struct { |
| 63 | Policy []string `json:"policy,omitempty"` |
| 64 | Members []string `json:"members,omitempty"` |
| 65 | MembersCleared bool `json:"membersCleared,omitempty"` |
| 66 | Warnings []string `json:"warnings,omitempty"` |
| 67 | FinalPolicy policy.Policy `json:"-"` |
| 68 | } |
| 69 | |
| 70 | // ConfigStore coordinates configuration with the tenant store, community |
| 71 | // service and policy callbacks. |
| 72 | type ConfigStore struct { |
| 73 | Store *storage.Store |
| 74 | Community *community.Service |
| 75 | Policy func() policy.Policy |
| 76 | // OnApplied runs only after the durable transaction commits. It is the |
| 77 | // integration point for replacing an in-memory policy and closing stale |
| 78 | // subscriptions. |
| 79 | OnApplied func(policy.Policy) |
| 80 | // ValidatePolicy runs before a config transaction is committed. |
| 81 | ValidatePolicy func(policy.Policy) error |
| 82 | } |
| 83 | |
| 84 | // ApplyOptions controls an import or preset application. |
| 85 | type ApplyOptions struct { |
| 86 | DryRun bool |
| 87 | MigrationOwner string |
| 88 | } |
| 89 | |
| 90 | func New(c ConfigStore) *ConfigStore { |
| 91 | if c.Store != nil { |
| 92 | _, _ = c.Store.DB().Exec(`CREATE TABLE IF NOT EXISTS config_connections(position INTEGER PRIMARY KEY, value TEXT NOT NULL)` ) |
| 93 | _, _ = c.Store.DB().Exec(`CREATE TABLE IF NOT EXISTS config_jobs(id TEXT PRIMARY KEY, recipe TEXT NOT NULL)` ) |
| 94 | _ = replication.EnsureJobSchema(c.Store.DB()) |
| 95 | } |
| 96 | return &c |
| 97 | } |
| 98 | |
| 99 | func Parse(raw []byte) (Config, error) { |
| 100 | var obj map[string]json.RawMessage |
| 101 | if err := json.Unmarshal(raw, &obj); err != nil { |
| 102 | return Config{}, fmt.Errorf("invalid: config JSON: %w" , err) |
| 103 | } |
| 104 | var f string |
| 105 | _ = json.Unmarshal(obj["format" ], &f) |
| 106 | if f != "bind.ws/relay-config/1" && f != Format { |
| 107 | return Config{}, errors.New("invalid: format must be bind.ws/relay-config/2" ) |
| 108 | } |
| 109 | var c Config |
| 110 | c.Format = Format |
| 111 | _ = json.Unmarshal(obj["name" ], &c.Name) |
| 112 | if b := obj["exported_at" ]; b != nil { |
| 113 | _ = json.Unmarshal(b, &c.ExportedAt) |
| 114 | } |
| 115 | for key, value := range obj { |
| 116 | switch key { |
| 117 | case "format" , "name" , "exported_at" : |
| 118 | case "policy" : |
| 119 | if err := json.Unmarshal(value, &c.Policy); err != nil { |
| 120 | return Config{}, errors.New("invalid: policy must be an object" ) |
| 121 | } |
| 122 | c.Sections = append(c.Sections, "policy" ) |
| 123 | warnPolicy(&c) |
| 124 | case "members" : |
| 125 | if err := json.Unmarshal(value, &c.Members); err != nil { |
| 126 | return Config{}, errors.New("invalid: members must be a list" ) |
| 127 | } |
| 128 | for _, member := range c.Members { |
| 129 | if len(member.PubKey) != 64 { |
| 130 | return Config{}, errors.New("invalid: member pubkey must be 64 hex chars" ) |
| 131 | } |
| 132 | } |
| 133 | c.Sections = append(c.Sections, "members" ) |
| 134 | case "bans" : |
| 135 | _ = json.Unmarshal(value, &c.Bans) |
| 136 | c.Sections = append(c.Sections, "bans" ) |
| 137 | case "addresses" : |
| 138 | _ = json.Unmarshal(value, &c.Addresses) |
| 139 | c.Sections = append(c.Sections, "addresses" ) |
| 140 | case "banned_events" : |
| 141 | _ = json.Unmarshal(value, &c.BannedEvents) |
| 142 | c.Sections = append(c.Sections, "banned_events" ) |
| 143 | case "kinds" : |
| 144 | if err := json.Unmarshal(value, &c.Kinds); err != nil { |
| 145 | return Config{}, err |
| 146 | } |
| 147 | c.Sections = append(c.Sections, "kinds" ) |
| 148 | case "retention" : |
| 149 | if err := json.Unmarshal(value, &c.Retention); err != nil { |
| 150 | return Config{}, err |
| 151 | } |
| 152 | c.Sections = append(c.Sections, "retention" ) |
| 153 | case "connections" : |
| 154 | if err := json.Unmarshal(value, &c.Connections); err != nil { |
| 155 | return Config{}, err |
| 156 | } |
| 157 | c.Sections = append(c.Sections, "connections" ) |
| 158 | case "jobs" : |
| 159 | if err := json.Unmarshal(value, &c.Jobs); err != nil { |
| 160 | return Config{}, errors.New("invalid: jobs must be a list" ) |
| 161 | } |
| 162 | c.Sections = append(c.Sections, "jobs" ) |
| 163 | default: |
| 164 | c.Warnings = append(c.Warnings, key+": not a setting" ) |
| 165 | } |
| 166 | } |
| 167 | sort.Strings(c.Sections) |
| 168 | return c, nil |
| 169 | } |
| 170 | |
| 171 | func warnPolicy(c *Config) { |
| 172 | for key := range c.Policy { |
| 173 | if key == "lease" { |
| 174 | c.Warnings = append(c.Warnings, "policy.lease: legacy lease imported disabled; explicit owner mapping required" ) |
| 175 | } else if key == "owner" { |
| 176 | c.Warnings = append(c.Warnings, "policy.owner: never imported; tenant owner must be assigned by the operator" ) |
| 177 | } else if key != "fileLimits" && (key == "succession" || key == "customHosts" || key == "fuel" || strings.Contains(strings.ToLower(key), "limit" ) || strings.HasPrefix(strings.ToLower(key), "free" ) || strings.HasPrefix(strings.ToLower(key), "sats" )) { |
| 178 | c.Warnings = append(c.Warnings, "policy." +key+": removed from self-hosted runtime" ) |
| 179 | } |
| 180 | } |
| 181 | } |
| 182 | |
| 183 | func (s *ConfigStore) Export(ctx context.Context) (Config, error) { |
| 184 | p := s.Policy() |
| 185 | raw, _ := json.Marshal(p) |
| 186 | var pm map[string]json.RawMessage |
| 187 | _ = json.Unmarshal(raw, &pm) |
| 188 | for _, key := range []string{"owner" , "succession" , "customHosts" , "fuel" , "lease" } { |
| 189 | delete(pm, key) |
| 190 | } |
| 191 | c := Config{Format: Format, ExportedAt: time.Now().Unix(), Policy: pm, Sections: []string{"policy" , "members" , "bans" , "addresses" , "banned_events" , "kinds" , "retention" , "connections" }, Warnings: []string{"identity private material is never exported" }} |
| 192 | if s.Store == nil { |
| 193 | return c, errors.New("config: missing store" ) |
| 194 | } |
| 195 | communityConfig, err := community.ExportConfig(ctx, s.Store) |
| 196 | if err != nil { |
| 197 | return c, err |
| 198 | } |
| 199 | for _, m := range communityConfig.Members { |
| 200 | c.Members = append(c.Members, Member{PubKey: m.PubKey, Name: m.Name, Note: m.Note, Role: m.Role}) |
| 201 | } |
| 202 | for _, item := range communityConfig.Bans { |
| 203 | c.Bans = append(c.Bans, map[string]any{"pubkey" : item.PubKey, "reason" : item.Reason}) |
| 204 | } |
| 205 | for _, item := range communityConfig.IPBlocks { |
| 206 | c.Addresses = append(c.Addresses, map[string]any{"ip" : item.IP, "reason" : item.Reason}) |
| 207 | } |
| 208 | for _, item := range communityConfig.EventBans { |
| 209 | c.BannedEvents = append(c.BannedEvents, map[string]any{"id" : item.ID, "reason" : item.Reason}) |
| 210 | } |
| 211 | for _, item := range communityConfig.KindRules { |
| 212 | if item.Rule == "allow" { |
| 213 | c.Kinds.Allow = append(c.Kinds.Allow, item.Kind) |
| 214 | } else if item.Rule == "block" { |
| 215 | c.Kinds.Block = append(c.Kinds.Block, item.Kind) |
| 216 | } |
| 217 | } |
| 218 | for _, item := range communityConfig.Retention { |
| 219 | c.Retention = append(c.Retention, Rule{Kind: item.Kind, Days: item.Days}) |
| 220 | } |
| 221 | rows, err := s.Store.DB().QueryContext(ctx, `SELECT value FROM config_connections ORDER BY position` ) |
| 222 | if err == nil { |
| 223 | defer rows.Close() |
| 224 | for rows.Next() { |
| 225 | var raw string |
| 226 | if err := rows.Scan(&raw); err != nil { |
| 227 | return c, err |
| 228 | } |
| 229 | var conn map[string]any |
| 230 | if json.Unmarshal([]byte(raw), &conn) == nil { |
| 231 | c.Connections = append(c.Connections, conn) |
| 232 | } |
| 233 | } |
| 234 | } |
| 235 | rows, err = s.Store.DB().QueryContext(ctx, `SELECT id,recipe FROM config_jobs ORDER BY id` ) |
| 236 | if err == nil { |
| 237 | defer rows.Close() |
| 238 | for rows.Next() { |
| 239 | var id, recipe string |
| 240 | if err := rows.Scan(&id, &recipe); err != nil { |
| 241 | return c, err |
| 242 | } |
| 243 | var job map[string]any |
| 244 | if json.Unmarshal([]byte(recipe), &job) != nil { |
| 245 | job = map[string]any{"recipe" : recipe} |
| 246 | } |
| 247 | job["id" ] = id |
| 248 | c.Jobs = append(c.Jobs, job) |
| 249 | } |
| 250 | if err := rows.Err(); err != nil { |
| 251 | return c, err |
| 252 | } |
| 253 | if len(c.Jobs) > 0 { |
| 254 | c.Sections = append(c.Sections, "jobs" ) |
| 255 | } |
| 256 | } |
| 257 | sort.Strings(c.Sections) |
| 258 | return c, nil |
| 259 | } |
| 260 | |
| 261 | func (s *ConfigStore) Plan(ctx context.Context, c Config) (Changes, error) { |
| 262 | if s.Policy == nil { |
| 263 | return Changes{}, errors.New("config: missing policy accessor" ) |
| 264 | } |
| 265 | cur := s.Policy() |
| 266 | ch := Changes{Warnings: append([]string(nil), c.Warnings...)} |
| 267 | next, err := policy.Patch(cur, c.Policy) |
| 268 | if err != nil { |
| 269 | return Changes{}, fmt.Errorf("config policy: %w" , err) |
| 270 | } |
| 271 | ch.FinalPolicy = next |
| 272 | for k := range c.Policy { |
| 273 | if fmt.Sprint(marshalRaw(cur, k)) != fmt.Sprint(marshalRaw(next, k)) { |
| 274 | ch.Policy = append(ch.Policy, k) |
| 275 | } |
| 276 | } |
| 277 | if has(c, "members" ) { |
| 278 | ch.MembersCleared = len(c.Members) == 0 |
| 279 | ch.Members = append(ch.Members, "replace members" ) |
| 280 | } |
| 281 | return ch, nil |
| 282 | } |
| 283 | |
| 284 | func finalPolicy(cur policy.Policy, c Config, migrationOwner string) (policy.Policy, error) { |
| 285 | next, err := policy.Patch(cur, c.Policy) |
| 286 | if err != nil { |
| 287 | return policy.Policy{}, fmt.Errorf("config policy: %w" , err) |
| 288 | } |
| 289 | // A legacy lease is evidence only. It can never silently become ownership. |
| 290 | // Existing tenants retain their explicit owner; unowned imports require an |
| 291 | // operator supplied mapping. |
| 292 | if _, leased := c.Policy["lease" ]; leased && cur.Owner == "" { |
| 293 | if migrationOwner == "" { |
| 294 | return policy.Policy{}, errors.New("config migration: leased relay has no owner; supply migrationOwner" ) |
| 295 | } |
| 296 | if len(migrationOwner) != 64 { |
| 297 | return policy.Policy{}, errors.New("config migration: migrationOwner must be a 64-hex pubkey" ) |
| 298 | } |
| 299 | if _, err := hex.DecodeString(migrationOwner); err != nil { |
| 300 | return policy.Policy{}, errors.New("config migration: migrationOwner must be a 64-hex pubkey" ) |
| 301 | } |
| 302 | next.Owner = migrationOwner |
| 303 | } |
| 304 | return next, nil |
| 305 | } |
| 306 | |
| 307 | func marshalRaw(p policy.Policy, key string) []byte { |
| 308 | b, _ := json.Marshal(p) |
| 309 | var m map[string]json.RawMessage |
| 310 | _ = json.Unmarshal(b, &m) |
| 311 | return m[key] |
| 312 | } |
| 313 | func has(c Config, key string) bool { |
| 314 | for _, x := range c.Sections { |
| 315 | if x == key { |
| 316 | return true |
| 317 | } |
| 318 | } |
| 319 | return false |
| 320 | } |
| 321 | |
| 322 | func (s *ConfigStore) Apply(ctx context.Context, c Config, dryRun bool) (Changes, error) { |
| 323 | return s.ApplyWithOptions(ctx, c, ApplyOptions{DryRun: dryRun}) |
| 324 | } |
| 325 | |
| 326 | func (s *ConfigStore) ApplyWithOptions(ctx context.Context, c Config, opts ApplyOptions) (Changes, error) { |
| 327 | ch, err := s.Plan(ctx, c) |
| 328 | if err != nil { |
| 329 | return Changes{}, err |
| 330 | } |
| 331 | cur := s.Policy() |
| 332 | ch.FinalPolicy, err = finalPolicy(cur, c, opts.MigrationOwner) |
| 333 | if err != nil { |
| 334 | return Changes{}, err |
| 335 | } |
| 336 | if s.ValidatePolicy != nil { |
| 337 | if err := s.ValidatePolicy(ch.FinalPolicy); err != nil { |
| 338 | return Changes{}, err |
| 339 | } |
| 340 | } |
| 341 | if opts.DryRun { |
| 342 | return ch, nil |
| 343 | } |
| 344 | if s.Store == nil { |
| 345 | return Changes{}, errors.New("config: missing store" ) |
| 346 | } |
| 347 | err = s.Store.WithTx(ctx, func(tx *sql.Tx) error { |
| 348 | // Persist the fully merged policy before touching metadata. All writes |
| 349 | // remain in this transaction so a later validation/constraint failure |
| 350 | // cannot leave the relay half-imported. |
| 351 | if has(c, "policy" ) || (ch.FinalPolicy.Owner != "" && s.Policy().Owner == "" ) { |
| 352 | if err := storage.PutSetting(ctx, tx, "policy" , ch.FinalPolicy); err != nil { |
| 353 | return err |
| 354 | } |
| 355 | } |
| 356 | communityConfig := community.ConfigSnapshot{} |
| 357 | for _, m := range c.Members { |
| 358 | communityConfig.Members = append(communityConfig.Members, community.Member{PubKey: m.PubKey, Name: m.Name, Note: m.Note, Role: m.Role}) |
| 359 | } |
| 360 | for _, b := range c.Bans { |
| 361 | pk, _ := b["pubkey" ].(string) |
| 362 | reason, _ := b["reason" ].(string) |
| 363 | communityConfig.Bans = append(communityConfig.Bans, community.ConfigBan{PubKey: pk, Reason: reason}) |
| 364 | } |
| 365 | for _, b := range c.Addresses { |
| 366 | ip, _ := b["ip" ].(string) |
| 367 | reason, _ := b["reason" ].(string) |
| 368 | communityConfig.IPBlocks = append(communityConfig.IPBlocks, community.ConfigIPBlock{IP: ip, Reason: reason}) |
| 369 | } |
| 370 | for _, b := range c.BannedEvents { |
| 371 | id, _ := b["id" ].(string) |
| 372 | reason, _ := b["reason" ].(string) |
| 373 | communityConfig.EventBans = append(communityConfig.EventBans, community.ConfigEventBan{ID: id, Reason: reason}) |
| 374 | } |
| 375 | for _, k := range c.Kinds.Allow { |
| 376 | communityConfig.KindRules = append(communityConfig.KindRules, community.ConfigKindRule{Kind: k, Rule: "allow" }) |
| 377 | } |
| 378 | for _, k := range c.Kinds.Block { |
| 379 | communityConfig.KindRules = append(communityConfig.KindRules, community.ConfigKindRule{Kind: k, Rule: "block" }) |
| 380 | } |
| 381 | for _, r := range c.Retention { |
| 382 | communityConfig.Retention = append(communityConfig.Retention, community.ConfigRetention{Kind: r.Kind, Days: r.Days}) |
| 383 | } |
| 384 | if err := community.ApplyConfigTx(ctx, tx, communityConfig, has(c, "members" ), has(c, "bans" ), has(c, "addresses" ), has(c, "banned_events" ), has(c, "kinds" ), has(c, "retention" )); err != nil { |
| 385 | return err |
| 386 | } |
| 387 | if s.Community != nil && ch.FinalPolicy.Owner != "" && ch.FinalPolicy.Owner != cur.Owner { |
| 388 | if err := s.Community.ApplyOwnerTx(ctx, tx, cur.Owner, ch.FinalPolicy.Owner); err != nil { |
| 389 | return err |
| 390 | } |
| 391 | } |
| 392 | if has(c, "connections" ) { |
| 393 | if _, err := tx.ExecContext(ctx, `DELETE FROM config_connections` ); err != nil { |
| 394 | return err |
| 395 | } |
| 396 | for i, conn := range c.Connections { |
| 397 | b, _ := json.Marshal(conn) |
| 398 | if _, err := tx.ExecContext(ctx, `INSERT INTO config_connections(position,value) VALUES(?,?)` , i, string(b)); err != nil { |
| 399 | return err |
| 400 | } |
| 401 | } |
| 402 | } |
| 403 | if has(c, "jobs" ) { |
| 404 | if _, err := tx.ExecContext(ctx, `DELETE FROM config_jobs` ); err != nil { |
| 405 | return err |
| 406 | } |
| 407 | var replicationJobs []replication.ConfigJob |
| 408 | for _, job := range c.Jobs { |
| 409 | id, _ := job["id" ].(string) |
| 410 | if id == "" { |
| 411 | return errors.New("config jobs: missing id" ) |
| 412 | } |
| 413 | copy := make(map[string]any, len(job)) |
| 414 | for k, v := range job { |
| 415 | if k != "id" { |
| 416 | copy[k] = v |
| 417 | } |
| 418 | } |
| 419 | recipe, err := json.Marshal(copy) |
| 420 | if err != nil { |
| 421 | return err |
| 422 | } |
| 423 | if _, err := tx.ExecContext(ctx, `INSERT INTO config_jobs(id,recipe) VALUES(?,?)` , id, string(recipe)); err != nil { |
| 424 | return err |
| 425 | } |
| 426 | copy["id" ] = id |
| 427 | payload, err := json.Marshal(copy) |
| 428 | if err != nil { |
| 429 | return err |
| 430 | } |
| 431 | replicationJobs = append(replicationJobs, replication.ConfigJob{ID: id, Payload: payload}) |
| 432 | } |
| 433 | if err := replication.ReplaceJobsTx(ctx, tx, replicationJobs); err != nil { |
| 434 | return err |
| 435 | } |
| 436 | } |
| 437 | return nil |
| 438 | }) |
| 439 | if err != nil { |
| 440 | return Changes{}, fmt.Errorf("config apply: %w" , err) |
| 441 | } |
| 442 | if ch.FinalPolicy.Owner != cur.Owner && s.Community != nil { |
| 443 | if err := s.Community.SetOwner(ch.FinalPolicy.Owner); err != nil { |
| 444 | return Changes{}, err |
| 445 | } |
| 446 | } |
| 447 | if (len(c.Policy) > 0 || ch.FinalPolicy.Owner != cur.Owner) && s.OnApplied != nil { |
| 448 | s.OnApplied(ch.FinalPolicy) |
| 449 | } |
| 450 | return ch, nil |
| 451 | } |
| 452 | |
| 453 | func Presets() []string { return templates.Names() } |
| 454 | |
| 455 | func (s *ConfigStore) Execute(ctx context.Context, method string, params []json.RawMessage) (any, error) { |
| 456 | switch method { |
| 457 | case "exportconfig" : |
| 458 | return s.Export(ctx) |
| 459 | case "importconfig" : |
| 460 | if len(params) == 0 { |
| 461 | return nil, errors.New("invalid: missing configuration" ) |
| 462 | } |
| 463 | var raw json.RawMessage = params[0] |
| 464 | c, err := Parse(raw) |
| 465 | if err != nil { |
| 466 | return nil, err |
| 467 | } |
| 468 | dry := false |
| 469 | migrationOwner := "" |
| 470 | if len(params) > 1 { |
| 471 | var o struct { |
| 472 | DryRun bool `json:"dryRun"` |
| 473 | MigrationOwner string `json:"migrationOwner"` |
| 474 | } |
| 475 | _ = json.Unmarshal(params[1], &o) |
| 476 | dry = o.DryRun |
| 477 | migrationOwner = o.MigrationOwner |
| 478 | } |
| 479 | return s.ApplyWithOptions(ctx, c, ApplyOptions{DryRun: dry, MigrationOwner: migrationOwner}) |
| 480 | case "applypreset" : |
| 481 | var name string |
| 482 | if len(params) == 0 || json.Unmarshal(params[0], &name) != nil { |
| 483 | return nil, errors.New("invalid: preset name required" ) |
| 484 | } |
| 485 | return s.ApplyPreset(ctx, name, false) |
| 486 | case "listpresets" : |
| 487 | out := []map[string]any{} |
| 488 | for _, name := range templates.Names() { |
| 489 | if t, ok := templates.Find(name); ok { |
| 490 | out = append(out, map[string]any{"name" : t.Name, "title" : t.Title, "about" : t.About, "source" : t.Source, "every" : t.EveryHours, "connections" : t.Connections}) |
| 491 | } |
| 492 | } |
| 493 | return out, nil |
| 494 | default: |
| 495 | return nil, fmt.Errorf("unsupported: unknown config method %q" , method) |
| 496 | } |
| 497 | } |
| 498 | |
| 499 | func (s *ConfigStore) ApplyPreset(ctx context.Context, name string, dryRun bool) (Changes, error) { |
| 500 | t, ok := templates.Find(name) |
| 501 | if !ok { |
| 502 | return Changes{}, fmt.Errorf("invalid: no preset named %s" , name) |
| 503 | } |
| 504 | p := policy.Defaults(s.Policy().Owner) |
| 505 | _ = p |
| 506 | raw, _ := json.Marshal(t.Policy) |
| 507 | var pm map[string]json.RawMessage |
| 508 | _ = json.Unmarshal(raw, &pm) |
| 509 | c := Config{Format: Format, Policy: pm, Sections: []string{"policy" }} |
| 510 | if len(t.AllowKinds) > 0 || len(t.BlockKinds) > 0 { |
| 511 | c.Sections = append(c.Sections, "kinds" ) |
| 512 | c.Kinds.Allow = t.AllowKinds |
| 513 | c.Kinds.Block = t.BlockKinds |
| 514 | } |
| 515 | if t.RetentionDays > 0 { |
| 516 | c.Sections = append(c.Sections, "retention" ) |
| 517 | c.Retention = []Rule{{Days: t.RetentionDays}} |
| 518 | } |
| 519 | if len(t.Connections) > 0 { |
| 520 | c.Sections = append(c.Sections, "connections" ) |
| 521 | for _, name := range t.Connections { |
| 522 | c.Connections = append(c.Connections, map[string]any{"template" : name}) |
| 523 | } |
| 524 | } |
| 525 | return s.Apply(ctx, c, dryRun) |
| 526 | } |