Skip to content

Commit 39f5df1

Browse files
briankaneclaude
andcommitted
feat: add CUE version compatibility upgrade engine (cue/upgrade)
Introduces the cue/upgrade package that transparently rewrites legacy CUE syntax (pre-v1.11 KubeVela definitions) at render time so that existing stored definitions continue to work against CUE v0.14+. Three upgrade rules are implemented: 1. list-arithmetic (upgrade_1_11.go) - `list + list` → `list.Concat([list, list])` - `list * n` → `list.Repeat(list, n)` Handles context.* chains (e.g. context.output.spec.template.spec.containers) and parameter.* selectors with defaulted list types. 2. error-field-label (upgrade_1_11.go) - Unquoted `error:` field label → `"error":` 3. bool-default-negation (upgrade_bool_default.go) - `_flag: bool | *false` + `if !_flag` guard pattern → direct CUE expression without the hidden bool field Also provides: - EnsureCueVersionCompatibility / RequiresUpgrade public API (upgrade.go) - EnableCUEVersionCompatibility feature flag - LRU result cache (cache.go) for repeated evaluations of the same template - Comprehensive test coverage including edge cases for selector chains Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Brian Kane <briankane1@gmail.com>
1 parent eeb20f6 commit 39f5df1

6 files changed

Lines changed: 3852 additions & 0 deletions

File tree

cue/upgrade/cache.go

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/*
2+
/*
3+
Copyright 2026 The KubeVela Authors.
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package upgrade
18+
19+
import (
20+
"container/list"
21+
"context"
22+
"crypto/sha256"
23+
"fmt"
24+
"sync"
25+
"sync/atomic"
26+
"time"
27+
)
28+
29+
// CompatibilityCacheSize is the maximum number of cache entries. Default 2000.
30+
// Overridden at startup via --cue-compatibility-cache-size.
31+
var CompatibilityCacheSize = 2000
32+
33+
// compatCache stores the result of each template compatibility check, keyed by SHA-256 of the raw template.
34+
var compatCache atomic.Pointer[lruCache]
35+
36+
// compatCacheCancel cancels the eviction goroutine of the current compatCache instance.
37+
var compatCacheCancel context.CancelFunc
38+
39+
// compatCacheMu serialises concurrent calls to InitCompatibilityCache.
40+
var compatCacheMu sync.Mutex
41+
42+
func init() {
43+
c := newLRUCache(CompatibilityCacheSize)
44+
c.startEvictionLoop(context.Background())
45+
compatCache.Store(c)
46+
}
47+
48+
// CacheEntryTTL is how long an unaccessed entry lives before being swept.
49+
var CacheEntryTTL = 1 * time.Hour
50+
51+
// InitCompatibilityCache reinitialises the cache with the given size and starts background TTL eviction.
52+
// Safe to call multiple times (e.g. in tests): the previous eviction goroutine is stopped first.
53+
func InitCompatibilityCache(ctx context.Context, size int) {
54+
if size < 0 {
55+
size = 0
56+
}
57+
compatCacheMu.Lock()
58+
defer compatCacheMu.Unlock()
59+
if compatCacheCancel != nil {
60+
compatCacheCancel()
61+
}
62+
CompatibilityCacheSize = size
63+
c := newLRUCache(CompatibilityCacheSize)
64+
if size > 0 {
65+
cacheCtx, cancel := context.WithCancel(ctx)
66+
compatCacheCancel = cancel
67+
c.startEvictionLoop(cacheCtx)
68+
} else {
69+
compatCacheCancel = nil
70+
}
71+
compatCache.Store(c)
72+
}
73+
74+
// templateHash returns the SHA-256 hex digest of s, used as the cache key.
75+
func templateHash(s string) string {
76+
sum := sha256.Sum256([]byte(s))
77+
return fmt.Sprintf("%x", sum)
78+
}
79+
80+
// compatEntry is the cached result for a single template.
81+
// upgraded always holds the normalised (and possibly semantically rewritten) template.
82+
// requiresUpgrade is true only when semantic fixes were applied.
83+
type compatEntry struct {
84+
requiresUpgrade bool
85+
upgraded string
86+
}
87+
88+
// lruCache is a goroutine-safe LRU cache with TTL eviction.
89+
type lruCache struct {
90+
mu sync.Mutex
91+
capacity int
92+
ll *list.List
93+
items map[string]*list.Element
94+
}
95+
96+
type lruEntry struct {
97+
key string
98+
value compatEntry
99+
lastAccess time.Time
100+
}
101+
102+
func newLRUCache(capacity int) *lruCache {
103+
return &lruCache{
104+
capacity: capacity,
105+
ll: list.New(),
106+
items: make(map[string]*list.Element, capacity),
107+
}
108+
}
109+
110+
func (c *lruCache) startEvictionLoop(ctx context.Context) {
111+
interval := CacheEntryTTL / 2
112+
if interval <= 0 {
113+
return
114+
}
115+
go func() {
116+
ticker := time.NewTicker(interval)
117+
defer ticker.Stop()
118+
for {
119+
select {
120+
case <-ctx.Done():
121+
return
122+
case <-ticker.C:
123+
c.evictStale()
124+
}
125+
}
126+
}()
127+
}
128+
129+
func (c *lruCache) evictStale() {
130+
c.mu.Lock()
131+
now := time.Now()
132+
evicted := 0
133+
for key, el := range c.items {
134+
if now.Sub(el.Value.(*lruEntry).lastAccess) > CacheEntryTTL {
135+
c.ll.Remove(el)
136+
delete(c.items, key)
137+
evicted++
138+
}
139+
}
140+
c.mu.Unlock()
141+
if evicted > 0 {
142+
if fn := OnCacheEviction; fn != nil {
143+
for range evicted {
144+
fn("ttl")
145+
}
146+
}
147+
}
148+
}
149+
150+
func (c *lruCache) get(key string) (compatEntry, bool) {
151+
c.mu.Lock()
152+
defer c.mu.Unlock()
153+
el, ok := c.items[key]
154+
if !ok {
155+
return compatEntry{}, false
156+
}
157+
entry := el.Value.(*lruEntry)
158+
entry.lastAccess = time.Now()
159+
c.ll.MoveToFront(el)
160+
return entry.value, true
161+
}
162+
163+
func (c *lruCache) put(key string, value compatEntry) {
164+
if c.capacity <= 0 {
165+
return
166+
}
167+
c.mu.Lock()
168+
if el, ok := c.items[key]; ok {
169+
entry := el.Value.(*lruEntry)
170+
entry.value = value
171+
entry.lastAccess = time.Now()
172+
c.ll.MoveToFront(el)
173+
c.mu.Unlock()
174+
return
175+
}
176+
evicted := false
177+
if c.ll.Len() >= c.capacity {
178+
oldest := c.ll.Back()
179+
if oldest != nil {
180+
c.ll.Remove(oldest)
181+
delete(c.items, oldest.Value.(*lruEntry).key)
182+
evicted = true
183+
}
184+
}
185+
el := c.ll.PushFront(&lruEntry{key: key, value: value, lastAccess: time.Now()})
186+
c.items[key] = el
187+
c.mu.Unlock()
188+
if evicted {
189+
if fn := OnCacheEviction; fn != nil {
190+
fn("capacity")
191+
}
192+
}
193+
}
194+
195+
func (c *lruCache) len() int {
196+
c.mu.Lock()
197+
defer c.mu.Unlock()
198+
return c.ll.Len()
199+
}

0 commit comments

Comments
 (0)