mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-19 12:36:39 +00:00
* fix(controller): decode old object for delete requests Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: modernize golang Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: modernize golang Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: modernize golang Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * fix: preserve ca-bundles injected from external providers Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add metadata enforcement Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * feat: add metadata enforcement Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * fix: add resourcepoolclaim validation Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * fix: add resourcepoolclaim validation Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * fix: add resourcepoolclaim validation Signed-off-by: Oliver Baehler <oliver@sudo-i.net> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: add resourcepoolclaim validation Signed-off-by: Oliver Baehler <oliver@sudo-i.net> --------- Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> Signed-off-by: Oliver Baehler <oliver@sudo-i.net> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
375 lines
6.5 KiB
Go
375 lines
6.5 KiB
Go
// Copyright 2020-2026 Project Capsule Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package cache
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
|
|
"github.com/projectcapsule/capsule/pkg/api/rules"
|
|
"github.com/projectcapsule/capsule/pkg/api/runtime"
|
|
)
|
|
|
|
type RuleSet struct {
|
|
ID string
|
|
Compiled []CompiledRule
|
|
}
|
|
|
|
type CompiledRule struct {
|
|
Match runtime.ExpressionMatch
|
|
|
|
// RegexID is empty when Match.Expression is empty.
|
|
RegexID string
|
|
|
|
AllowedPolicy map[corev1.PullPolicy]struct{} // nil/empty => allow any
|
|
}
|
|
|
|
func (r *CompiledRule) AllowsPullPolicy(pullPolicy corev1.PullPolicy) bool {
|
|
if len(r.AllowedPolicy) == 0 {
|
|
return true
|
|
}
|
|
|
|
_, ok := r.AllowedPolicy[pullPolicy]
|
|
|
|
return ok
|
|
}
|
|
|
|
type RegistryRuleSetCache struct {
|
|
regexCache *RegexCache
|
|
|
|
mu sync.RWMutex
|
|
rs map[string]*RuleSet
|
|
}
|
|
|
|
func NewRegistryRuleSetCache(regexCache *RegexCache) *RegistryRuleSetCache {
|
|
if regexCache == nil {
|
|
regexCache = NewRegexCache()
|
|
}
|
|
|
|
return &RegistryRuleSetCache{
|
|
regexCache: regexCache,
|
|
rs: make(map[string]*RuleSet),
|
|
}
|
|
}
|
|
|
|
func (c *RegistryRuleSetCache) GetOrBuild(specRules []rules.OCIRegistry) (rs *RuleSet, fromCache bool, err error) {
|
|
if len(specRules) == 0 {
|
|
return nil, false, nil
|
|
}
|
|
|
|
if c == nil {
|
|
return nil, false, fmt.Errorf("registry rule set cache is nil")
|
|
}
|
|
|
|
id := c.HashRules(specRules)
|
|
|
|
c.mu.RLock()
|
|
rs = c.rs[id]
|
|
c.mu.RUnlock()
|
|
|
|
if rs != nil {
|
|
return rs, true, nil
|
|
}
|
|
|
|
built, err := c.buildRuleSet(id, specRules)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
if c.rs == nil {
|
|
c.rs = make(map[string]*RuleSet)
|
|
}
|
|
|
|
if rs = c.rs[id]; rs != nil {
|
|
return rs, true, nil
|
|
}
|
|
|
|
c.rs[id] = built
|
|
|
|
return built, false, nil
|
|
}
|
|
|
|
// Match matches reference, expression and pullPolicy.
|
|
// Admission evaluation should usually use MatchReference instead.
|
|
func (c *RegistryRuleSetCache) Match(
|
|
specRules []rules.OCIRegistry,
|
|
reference string,
|
|
pullPolicy corev1.PullPolicy,
|
|
) (*CompiledRule, error) {
|
|
rs, _, err := c.GetOrBuild(specRules)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if rs == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
return c.MatchRuleSet(rs, reference, pullPolicy)
|
|
}
|
|
|
|
func (c *RegistryRuleSetCache) MatchRuleSet(
|
|
rs *RuleSet,
|
|
reference string,
|
|
pullPolicy corev1.PullPolicy,
|
|
) (*CompiledRule, error) {
|
|
if c == nil {
|
|
return nil, fmt.Errorf("registry rule set cache is nil")
|
|
}
|
|
|
|
if c.regexCache == nil {
|
|
return nil, fmt.Errorf("regex cache is nil")
|
|
}
|
|
|
|
if rs == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
for i := range rs.Compiled {
|
|
rule := &rs.Compiled[i]
|
|
|
|
if !rule.AllowsPullPolicy(pullPolicy) {
|
|
continue
|
|
}
|
|
|
|
matched, err := rule.Match.MatchesWithExpressionMatcher(c.regexCache, reference)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if matched {
|
|
return rule, nil
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// MatchReference matches reference only.
|
|
// It intentionally does not check pullPolicy.
|
|
func (c *RegistryRuleSetCache) MatchReference(
|
|
rs *RuleSet,
|
|
reference string,
|
|
) (*CompiledRule, error) {
|
|
if c == nil {
|
|
return nil, fmt.Errorf("registry rule set cache is nil")
|
|
}
|
|
|
|
if c.regexCache == nil {
|
|
return nil, fmt.Errorf("regex cache is nil")
|
|
}
|
|
|
|
if rs == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
for i := range rs.Compiled {
|
|
rule := &rs.Compiled[i]
|
|
|
|
matched, err := rule.Match.MatchesWithExpressionMatcher(c.regexCache, reference)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if matched {
|
|
return rule, nil
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func (c *RegistryRuleSetCache) Stats() int {
|
|
if c == nil {
|
|
return 0
|
|
}
|
|
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
|
|
return len(c.rs)
|
|
}
|
|
|
|
func (c *RegistryRuleSetCache) PruneActive(activeIDs map[string]struct{}) int {
|
|
if c == nil {
|
|
return 0
|
|
}
|
|
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
removed := 0
|
|
|
|
for id := range c.rs {
|
|
if _, ok := activeIDs[id]; ok {
|
|
continue
|
|
}
|
|
|
|
delete(c.rs, id)
|
|
|
|
removed++
|
|
}
|
|
|
|
return removed
|
|
}
|
|
|
|
func (c *RegistryRuleSetCache) HashRules(specRules []rules.OCIRegistry) string {
|
|
var b strings.Builder
|
|
|
|
b.Grow(len(specRules) * 160)
|
|
|
|
const (
|
|
sepRule = "\n"
|
|
sepField = "\x1f"
|
|
sepList = "\x1e"
|
|
)
|
|
|
|
for _, r := range specRules {
|
|
match := r.ExpressionMatch
|
|
|
|
exact := append([]string(nil), match.Exact...)
|
|
sort.Strings(exact)
|
|
|
|
policies := make([]string, 0, len(r.Policy))
|
|
for _, p := range r.Policy {
|
|
policies = append(policies, strings.TrimSpace(string(p)))
|
|
}
|
|
|
|
sort.Strings(policies)
|
|
|
|
b.WriteString("exact")
|
|
b.WriteString(sepField)
|
|
|
|
for i, v := range exact {
|
|
if i > 0 {
|
|
b.WriteString(sepList)
|
|
}
|
|
|
|
b.WriteString(v)
|
|
}
|
|
|
|
b.WriteString(sepField)
|
|
b.WriteString("exp")
|
|
b.WriteString(sepField)
|
|
b.WriteString(strings.TrimSpace(match.Expression))
|
|
|
|
b.WriteString(sepField)
|
|
b.WriteString("negate")
|
|
b.WriteString(sepField)
|
|
|
|
if match.Negate {
|
|
b.WriteString("1")
|
|
} else {
|
|
b.WriteString("0")
|
|
}
|
|
|
|
b.WriteString(sepField)
|
|
b.WriteString("policy")
|
|
b.WriteString(sepField)
|
|
|
|
for i, p := range policies {
|
|
if i > 0 {
|
|
b.WriteString(sepList)
|
|
}
|
|
|
|
b.WriteString(p)
|
|
}
|
|
|
|
b.WriteString(sepRule)
|
|
}
|
|
|
|
sum := sha256.Sum256([]byte(b.String()))
|
|
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func (c *RegistryRuleSetCache) Has(id string) bool {
|
|
if c == nil {
|
|
return false
|
|
}
|
|
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
|
|
_, ok := c.rs[id]
|
|
|
|
return ok
|
|
}
|
|
|
|
func (c *RegistryRuleSetCache) Reset() {
|
|
if c == nil {
|
|
return
|
|
}
|
|
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
c.rs = make(map[string]*RuleSet)
|
|
}
|
|
|
|
//nolint:unused
|
|
func (c *RegistryRuleSetCache) insertForTest(id string) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
if c.rs == nil {
|
|
c.rs = make(map[string]*RuleSet)
|
|
}
|
|
|
|
c.rs[id] = &RuleSet{ID: id}
|
|
}
|
|
|
|
func (c *RegistryRuleSetCache) buildRuleSet(id string, specRules []rules.OCIRegistry) (*RuleSet, error) {
|
|
if c.regexCache == nil {
|
|
return nil, fmt.Errorf("regex cache is nil")
|
|
}
|
|
|
|
rs := &RuleSet{
|
|
ID: id,
|
|
Compiled: make([]CompiledRule, 0, len(specRules)),
|
|
}
|
|
|
|
for _, r := range specRules {
|
|
match := r.ExpressionMatch
|
|
|
|
if len(match.Exact) == 0 && strings.TrimSpace(match.Expression) == "" {
|
|
return nil, fmt.Errorf("registry rule must define at least one of exact or exp")
|
|
}
|
|
|
|
cr := CompiledRule{
|
|
Match: match,
|
|
}
|
|
|
|
if strings.TrimSpace(match.Expression) != "" {
|
|
compiled, _, err := c.regexCache.GetOrCompile(match.ExpressionRegex)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cr.RegexID = compiled.ID
|
|
}
|
|
|
|
if len(r.Policy) > 0 {
|
|
cr.AllowedPolicy = make(map[corev1.PullPolicy]struct{}, len(r.Policy))
|
|
|
|
for _, p := range r.Policy {
|
|
cr.AllowedPolicy[p] = struct{}{}
|
|
}
|
|
}
|
|
|
|
rs.Compiled = append(rs.Compiled, cr)
|
|
}
|
|
|
|
return rs, nil
|
|
}
|