mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-29 09:14:00 +00:00
feat(secubox): add profile inheritance merger
- Implement Merger with Resolve() for inheritance chain - Merge packages, kernel, services, sysctl, features - Add tier-lite, tier-standard, tier-pro profiles - Handle excluded packages removal Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
c9da45fa21
commit
2bbecfdc5f
143
cmd/secubox/internal/profile/merger.go
Normal file
143
cmd/secubox/internal/profile/merger.go
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
// cmd/secubox/internal/profile/merger.go
|
||||
package profile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// Merger resolves profile inheritance
|
||||
type Merger struct {
|
||||
profilesDir string
|
||||
cache map[string]*Profile
|
||||
}
|
||||
|
||||
// NewMerger creates a new profile merger
|
||||
func NewMerger(profilesDir string) *Merger {
|
||||
return &Merger{
|
||||
profilesDir: profilesDir,
|
||||
cache: make(map[string]*Profile),
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve loads a profile and merges all inherited profiles
|
||||
func (m *Merger) Resolve(name string) (*Profile, error) {
|
||||
// Check cache
|
||||
if p, ok := m.cache[name]; ok {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Load profile
|
||||
path := filepath.Join(m.profilesDir, name+".yaml")
|
||||
p, err := Load(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load profile %s: %w", name, err)
|
||||
}
|
||||
|
||||
// If no inheritance, return as-is
|
||||
if p.Inherits == "" {
|
||||
m.cache[name] = p
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Resolve parent first
|
||||
parent, err := m.Resolve(p.Inherits)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolve parent %s: %w", p.Inherits, err)
|
||||
}
|
||||
|
||||
// Merge parent into child
|
||||
merged := m.merge(parent, p)
|
||||
m.cache[name] = merged
|
||||
return merged, nil
|
||||
}
|
||||
|
||||
// merge combines parent and child profiles (child overrides parent)
|
||||
func (m *Merger) merge(parent, child *Profile) *Profile {
|
||||
result := &Profile{
|
||||
Name: child.Name,
|
||||
Description: child.Description,
|
||||
}
|
||||
|
||||
// Merge packages (combine required, child excluded wins)
|
||||
result.Packages.Required = append([]string{}, parent.Packages.Required...)
|
||||
result.Packages.Required = append(result.Packages.Required, child.Packages.Required...)
|
||||
result.Packages.Required = unique(result.Packages.Required)
|
||||
result.Packages.Excluded = child.Packages.Excluded
|
||||
|
||||
// Remove excluded packages from required
|
||||
result.Packages.Required = subtract(result.Packages.Required, result.Packages.Excluded)
|
||||
|
||||
// Kernel: child overrides or inherits
|
||||
result.Kernel = parent.Kernel
|
||||
if child.Kernel.Version != "" {
|
||||
result.Kernel.Version = child.Kernel.Version
|
||||
}
|
||||
if len(child.Kernel.Modules.Enable) > 0 {
|
||||
result.Kernel.Modules.Enable = append(result.Kernel.Modules.Enable, child.Kernel.Modules.Enable...)
|
||||
}
|
||||
if len(child.Kernel.Modules.Blacklist) > 0 {
|
||||
result.Kernel.Modules.Blacklist = child.Kernel.Modules.Blacklist
|
||||
}
|
||||
|
||||
// Services: merge
|
||||
result.Services.Enable = append(parent.Services.Enable, child.Services.Enable...)
|
||||
result.Services.Enable = unique(result.Services.Enable)
|
||||
result.Services.Disable = child.Services.Disable
|
||||
|
||||
// Sysctl: merge maps
|
||||
result.Sysctl = make(map[string]interface{})
|
||||
for k, v := range parent.Sysctl {
|
||||
result.Sysctl[k] = v
|
||||
}
|
||||
for k, v := range child.Sysctl {
|
||||
result.Sysctl[k] = v
|
||||
}
|
||||
|
||||
// Features: child overrides
|
||||
result.Features = parent.Features
|
||||
if child.Features.DPI != nil {
|
||||
result.Features.DPI = child.Features.DPI
|
||||
}
|
||||
if child.Features.Swap != "" {
|
||||
result.Features.Swap = child.Features.Swap
|
||||
}
|
||||
// LXC: explicit check since false is valid
|
||||
result.Features.LXC = child.Features.LXC || parent.Features.LXC
|
||||
|
||||
// Constraints: child overrides
|
||||
result.Constraints = child.Constraints
|
||||
if result.Constraints.MinRAM == "" {
|
||||
result.Constraints.MinRAM = parent.Constraints.MinRAM
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// unique removes duplicates from a slice
|
||||
func unique(s []string) []string {
|
||||
seen := make(map[string]bool)
|
||||
result := []string{}
|
||||
for _, v := range s {
|
||||
if !seen[v] {
|
||||
seen[v] = true
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// subtract removes items in b from a
|
||||
func subtract(a, b []string) []string {
|
||||
exclude := make(map[string]bool)
|
||||
for _, v := range b {
|
||||
exclude[v] = true
|
||||
}
|
||||
result := []string{}
|
||||
for _, v := range a {
|
||||
if !exclude[v] {
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
@ -34,3 +34,48 @@ packages:
|
|||
t.Errorf("Packages.Required = %d, want 2", len(p.Packages.Required))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeProfiles(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
// Create base profile
|
||||
base := `
|
||||
name: base
|
||||
packages:
|
||||
required:
|
||||
- secubox-core
|
||||
kernel:
|
||||
version: "6.6"
|
||||
`
|
||||
basePath := filepath.Join(dir, "base.yaml")
|
||||
os.WriteFile(basePath, []byte(base), 0644)
|
||||
|
||||
// Create child profile
|
||||
child := `
|
||||
name: child
|
||||
inherits: base
|
||||
packages:
|
||||
required:
|
||||
- secubox-crowdsec
|
||||
excluded:
|
||||
- secubox-dpi
|
||||
`
|
||||
childPath := filepath.Join(dir, "child.yaml")
|
||||
os.WriteFile(childPath, []byte(child), 0644)
|
||||
|
||||
merger := NewMerger(dir)
|
||||
result, err := merger.Resolve("child")
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve() error = %v", err)
|
||||
}
|
||||
|
||||
// Should have both base and child packages
|
||||
if len(result.Packages.Required) != 2 {
|
||||
t.Errorf("Packages.Required = %v, want 2 items", result.Packages.Required)
|
||||
}
|
||||
|
||||
// Should inherit kernel version
|
||||
if result.Kernel.Version != "6.6" {
|
||||
t.Errorf("Kernel.Version = %q, want %q", result.Kernel.Version, "6.6")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
27
profiles/tier-lite.yaml
Normal file
27
profiles/tier-lite.yaml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# profiles/tier-lite.yaml
|
||||
name: tier-lite
|
||||
inherits: base
|
||||
description: For 1-2GB RAM devices (ESPRESSObin)
|
||||
|
||||
constraints:
|
||||
min_ram: 1G
|
||||
max_ram: 2G
|
||||
|
||||
packages:
|
||||
required:
|
||||
- secubox-crowdsec
|
||||
- secubox-wireguard
|
||||
- secubox-netmodes
|
||||
- secubox-nac
|
||||
excluded:
|
||||
- secubox-dpi
|
||||
- secubox-ollama
|
||||
- secubox-jellyfin
|
||||
- secubox-matrix
|
||||
- secubox-nextcloud
|
||||
- secubox-gitea
|
||||
|
||||
features:
|
||||
dpi: false
|
||||
lxc: false
|
||||
swap: 512M
|
||||
16
profiles/tier-pro.yaml
Normal file
16
profiles/tier-pro.yaml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# profiles/tier-pro.yaml
|
||||
name: tier-pro
|
||||
inherits: base
|
||||
description: For 8GB+ RAM devices (MOCHAbin)
|
||||
|
||||
constraints:
|
||||
min_ram: 8G
|
||||
|
||||
packages:
|
||||
required:
|
||||
- secubox-full
|
||||
|
||||
features:
|
||||
dpi: inline
|
||||
lxc: true
|
||||
swap: "0"
|
||||
25
profiles/tier-standard.yaml
Normal file
25
profiles/tier-standard.yaml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# profiles/tier-standard.yaml
|
||||
name: tier-standard
|
||||
inherits: base
|
||||
description: For 4GB RAM devices
|
||||
|
||||
constraints:
|
||||
min_ram: 4G
|
||||
max_ram: 8G
|
||||
|
||||
packages:
|
||||
required:
|
||||
- secubox-crowdsec
|
||||
- secubox-wireguard
|
||||
- secubox-netmodes
|
||||
- secubox-nac
|
||||
- secubox-dpi
|
||||
- secubox-qos
|
||||
- secubox-waf
|
||||
- secubox-haproxy
|
||||
- secubox-vhost
|
||||
|
||||
features:
|
||||
dpi: mirror
|
||||
lxc: true
|
||||
swap: "0"
|
||||
Loading…
Reference in New Issue
Block a user