Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
607f226
feat: record backup consistency and captured volume coverage
Baylem Oct 21, 2026
90f75f1
merge: integrate stopped-source backup consistency
Baylem Oct 21, 2026
adb37ab
merge: include CSI restore routing correction
Baylem Oct 21, 2026
afb4eaa
merge: include shared web build prerequisite
Baylem Oct 21, 2026
4ef2b27
feat(operator): stage immutable template revisions per server
Baylem Oct 21, 2026
34e8a25
merge: preserve backup preparation and pinned coverage metadata
Baylem Oct 21, 2026
e0d67c6
merge: validate staged revisions against workload security
Baylem Oct 21, 2026
7ee7966
merge: retain configurable resources in staged workload inputs
Baylem Oct 21, 2026
8434483
fix: include capture output confinement follow-up
Baylem Oct 21, 2026
2c16b6e
fix: include backup agent interface CI corrections
Baylem Oct 21, 2026
2cffa4d
ci: require and report every audit E2E bucket
Baylem Oct 21, 2026
7ff8955
test: model backup status writes in revision fixtures
Baylem Oct 21, 2026
95e38d0
fix: carry capture verification fixes into staged rollouts
Baylem Oct 21, 2026
69c73eb
fix: carry operator upgrade migration into staged rollouts
Baylem Oct 21, 2026
3ab227e
fix(e2e): isolate rollout fixtures and observe workload stops
Baylem Oct 22, 2026
0e7282d
fix(operator): preserve condition changes before status comparison
Baylem Oct 22, 2026
4d2bfc6
fix: include restricted upgrade corrections in staged rollouts
Baylem Oct 22, 2026
db5db7c
fix(operator): retain failed restore ownership until startup drains
Baylem Oct 23, 2026
f118220
fix(ci): retain backup and rollout coverage with upstream selection
Baylem Oct 23, 2026
0e0d5d9
test: preserve controller updates in lifecycle fixtures
Baylem Oct 23, 2026
2081831
test: synchronize closed RCON response fixtures
Baylem Oct 23, 2026
575a912
test: verify shared PID survives restricted capture
Baylem Oct 24, 2026
32addfe
fix: allow guarded wipe recovery to drain workloads
Baylem Oct 24, 2026
dd8c9bb
fix: carry reviewed security and chart prerequisites
Baylem Oct 25, 2026
38c69aa
chore: reconcile staged rollouts with upstream and CI lint
Baylem Oct 25, 2026
22494f1
chore: reconcile staged rollouts and refresh CI inputs
Baylem Oct 27, 2026
ecee255
chore: reconcile rollout CRDs with upstream
Baylem Oct 28, 2026
52aae3f
Merge upstream and address backup and capture recovery feedback
Baylem Oct 31, 2026
7cf80d3
merge: stack staged rollouts on current security and backup branches
Baylem Nov 4, 2026
32c8f2b
fix(api): redact config with verified effective template schemas
Baylem Nov 4, 2026
0da2fa6
fix(web): synchronize backup coverage Pencil designs
Baylem Nov 4, 2026
27ba4d9
merge: carry diagnostics namespace isolation into staged rollouts
Baylem Nov 4, 2026
d1eb0c9
merge: protect pinned server configuration across API operations
Baylem Nov 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 49 additions & 14 deletions api/internal/handlers/config_redact.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ package handlers

import (
"context"
"errors"
"sync"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

"github.com/GameplanePanel/gameplane/api/internal/kube"
Expand Down Expand Up @@ -58,32 +58,43 @@ func rulesFromTemplate(tmpl *unstructured.Unstructured) configRules {
type configRuleCache struct {
k *kube.Client
mu sync.Mutex
entries map[string]configRules
entries map[configRuleKey]configRules
}

// A revision is content-addressed, but snapshot ownership belongs to one
// server. Never reuse a verified snapshot for a replacement or another server.
type configRuleKey struct {
template, namespace, name, uid, revision string
}

func newConfigRuleCache(k *kube.Client) *configRuleCache {
return &configRuleCache{k: k, entries: map[string]configRules{}}
return &configRuleCache{k: k, entries: map[configRuleKey]configRules{}}
}

// rulesFor returns the rules for the named template, failing closed when the
// template cannot be read.
func (c *configRuleCache) rulesFor(ctx context.Context, templateName string) configRules {
if templateName == "" || c == nil || c.k == nil || c.k.Dynamic == nil {
// rulesFor uses the server's admitted template, failing closed when that
// template cannot be read or its pinned snapshot cannot be verified.
func (c *configRuleCache) rulesFor(ctx context.Context, gs *unstructured.Unstructured) configRules {
if gs == nil || c == nil || c.k == nil {
return configRules{failClosed: true}
}
templateName, _, _ := unstructured.NestedString(gs.Object, "spec", "templateRef", "name")
key := configRuleKey{template: templateName, namespace: gs.GetNamespace(), name: gs.GetName(), uid: string(gs.GetUID()), revision: gs.GetAnnotations()[kube.EffectiveTemplateRevisionAnnotation]}
c.mu.Lock()
cached, ok := c.entries[templateName]
cached, ok := c.entries[key]
c.mu.Unlock()
if ok {
return cached
}
tmpl, err := c.k.Dynamic.Resource(kube.GVRs["templates"]).Get(ctx, templateName, metav1.GetOptions{})
tmpl, err := c.k.EffectiveGameTemplate(ctx, gs)
if err != nil {
return configRules{failClosed: true}
}
rules := rulesFromTemplate(tmpl)
if rules.failClosed {
return rules
}
c.mu.Lock()
c.entries[templateName] = rules
c.entries[key] = rules
c.mu.Unlock()
return rules
}
Expand All @@ -107,8 +118,7 @@ func (c *configRuleCache) redact(ctx context.Context, gs *unstructured.Unstructu
if len(cfg) == 0 {
return
}
name, _, _ := unstructured.NestedString(gs.Object, "spec", "templateRef", "name")
rules := c.rulesFor(ctx, name)
rules := c.rulesFor(ctx, gs)
for key, val := range cfg {
if !rules.redacts(key) {
continue
Expand All @@ -127,6 +137,32 @@ func (c *configRuleCache) redactList(ctx context.Context, list *unstructured.Uns
}
}

// validateCloneConfig prevents secrets copied from a pinned source from
// becoming ordinary config on an unpinned clone. Redacting only the POST reply
// would still expose them on the clone's subsequent GET/list/event responses.
func (c *configRuleCache) validateCloneConfig(ctx context.Context, source, clone *unstructured.Unstructured) error {
if source.GetAnnotations()[kube.EffectiveTemplateRevisionAnnotation] == "" {
return nil
}
cfg, _, err := unstructured.NestedMap(source.Object, "spec", "config")
if err != nil {
return errors.New("cannot clone: source config is malformed")
}
if len(cfg) == 0 {
return nil
}
sourceRules, targetRules := c.rulesFor(ctx, source), c.rulesFor(ctx, clone)
for key, value := range cfg {
if s, ok := value.(string); ok && s == "" {
continue
}
if sourceRules.redacts(key) && (targetRules.failClosed || !targetRules.password[key]) {
return errors.New("cannot clone: the current template does not protect copied passwords; restore its password field types or create a new server with fresh configuration")
}
}
return nil
}

// restoreRedactedConfig rewrites desired.spec.config before a PUT so redacted
// values the client sent back do not overwrite stored passwords. live is the
// stored object (nil when it does not exist). The rules are documented in
Expand All @@ -146,8 +182,7 @@ func restoreRedactedConfig(ctx context.Context, c *configRuleCache, desired, liv
return
}
if live != nil {
name, _, _ := unstructured.NestedString(live.Object, "spec", "templateRef", "name")
rules = c.rulesFor(ctx, name)
rules = c.rulesFor(ctx, live)
}
out := make(map[string]any, len(incoming))
for key, val := range incoming {
Expand Down
Loading
Loading