-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcache.go
More file actions
317 lines (278 loc) · 12.7 KB
/
Copy pathcache.go
File metadata and controls
317 lines (278 loc) · 12.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
package api
import (
"context"
"strconv"
"strings"
"time"
"unicode/utf8"
"github.com/moonrhythm/validator"
)
// Cache manages a project's edge cache-override zone: a single set of
// CEL-filtered cache policy overrides per project per location, applied at the
// parapet edge response cache. A zone maps 1:1 onto a parapet cache zone
// ConfigMap (label parapet.moonrhythm.io/cache: zone) in the location's
// cluster, bound to the project's ingresses via the
// parapet.moonrhythm.io/cache-zone annotation; the overrides map onto parapet's
// cacherule.Override. See the parapet-ingress-controller CACHE.md for the
// engine and evaluation order.
//
// There is at most one zone per (project, location), so it is addressed by
// project + location (no name). Set upserts the whole override set; Delete
// removes the zone entirely.
//
// Cache overrides are EDGE-ONLY (consumed by the edge control plane, never the
// in-cluster controller) and are a distinct capability from the WAF: a role can
// hold cache.* without waf.*. The platform-owned global baseline is not exposed
// here — it is operated in the controller's own namespace and is always
// authoritative over the zone.
type Cache interface {
// Get requires the `cache.get` permission.
Get(ctx context.Context, m *CacheGet) (*CacheItem, error)
// List requires the `cache.list` permission.
List(ctx context.Context, m *CacheList) (*CacheListResult, error)
// Set requires the `cache.set` permission.
Set(ctx context.Context, m *CacheSet) (*Empty, error)
// Delete requires the `cache.delete` permission.
Delete(ctx context.Context, m *CacheDelete) (*Empty, error)
// Metrics requires the `cache.get` permission.
Metrics(ctx context.Context, m *CacheMetrics) (*CacheMetricsResult, error)
}
// CacheOverride mirrors parapet's cacherule.Override: one cache-policy override
// evaluated for every request the zone covers. The yaml tags are snake_case
// because the deployer marshals this struct straight into the parapet
// `overrides:` ConfigMap document — the yaml form IS the parapet wire contract.
// Validation here mirrors parapet's SetOverrides checks so a batch the API
// accepts also compiles in the controller (all-or-nothing).
//
// ID is server-managed and project-local exactly like WAFRule.ID: send "" for a
// new override and the server generates one; echo the existing id (from
// Get/List) to keep an override's identity (and its metric series) across
// edits.
type CacheOverride struct {
ID string `json:"id" yaml:"id"`
Description string `json:"description" yaml:"description"`
// Action is "cache" (default; force a caching policy onto the fill) or
// "bypass" (the request skips the cache entirely). ttl/policy/status/stale_*
// are valid only for action=cache.
Action string `json:"action" yaml:"action"`
// Filter is an optional CEL expression (the same request.* surface as
// WAFRule.Expression) that scopes the override: empty means every request.
// request.body is always "" here (the cache does not buffer the body). A
// runtime eval error biases toward NOT caching (the inverse of the rate
// limiter's fail-open, because caching is the dangerous action). Validated
// structurally here and compiled all-or-nothing by the controller.
Filter string `json:"filter" yaml:"filter,omitempty"`
// TTL is the forced freshness lifetime (a Go duration, 1s..CacheMaxTTL).
// Required for action=cache; rejected for action=bypass.
TTL string `json:"ttl" yaml:"ttl,omitempty"`
// Policy selects how far the force reaches over the origin's Cache-Control:
// "conservative", "balanced" (default), or "aggressive" (overrides almost
// everything, including the Authorization gate — a cross-user-leak risk; see
// CACHE.md). action=cache only.
Policy string `json:"policy" yaml:"policy,omitempty"`
// StaleWhileRevalidate / StaleIfError force the RFC 5861 windows (Go
// durations) for this rule's fills. They ride the forced policy, so they
// require a ttl. action=cache only.
StaleWhileRevalidate string `json:"staleWhileRevalidate" yaml:"stale_while_revalidate,omitempty"`
StaleIfError string `json:"staleIfError" yaml:"stale_if_error,omitempty"`
// Status narrows a force to specific origin response statuses. Empty means
// "every cacheable status the cache already accepts". action=cache only.
Status []int `json:"status" yaml:"status,omitempty"`
// Mode is "enforce" (default) or "shadow": shadow evaluates and counts the
// override but never changes caching, so a rule can be validated against live
// traffic before it takes effect.
Mode string `json:"mode" yaml:"mode,omitempty"`
// Priority orders force rules; the first matching cache rule wins (lower
// number first, declaration order breaks ties). 0 is resolved to the parapet
// default (100). Bypass rules are not ordered against each other.
Priority int `json:"priority" yaml:"priority"`
}
var validCachePolicies = map[string]bool{"": true, "conservative": true, "balanced": true, "aggressive": true}
// validCacheOverrides validates the structural contract of an override set,
// mirroring parapet's SetOverrides checks so a batch the API accepts also
// compiles in the controller (all-or-nothing, like the WAF rules/limits).
func validCacheOverrides(v *validator.Validator, overrides []CacheOverride) {
v.Mustf(len(overrides) <= CacheMaxOverrides, "overrides must not exceed %d overrides", CacheMaxOverrides)
seen := make(map[string]bool, len(overrides))
for i := range overrides {
o := &overrides[i]
o.ID = strings.TrimSpace(o.ID)
o.Action = strings.TrimSpace(o.Action)
o.Filter = strings.TrimSpace(o.Filter)
o.TTL = strings.TrimSpace(o.TTL)
o.Policy = strings.TrimSpace(o.Policy)
o.StaleWhileRevalidate = strings.TrimSpace(o.StaleWhileRevalidate)
o.StaleIfError = strings.TrimSpace(o.StaleIfError)
o.Mode = strings.TrimSpace(o.Mode)
ref := o.ID
if ref == "" {
ref = "#" + strconv.Itoa(i)
}
// ID is server-managed (see CacheOverride): "" means "generate one".
if o.ID != "" {
v.Mustf(ReValidWAFRuleID.MatchString(o.ID), "override %s: id invalid "+ReValidWAFRuleIDStr, ref)
v.Mustf(utf8.RuneCountInString(o.ID) <= CacheMaxOverrideIDLength, "override %s: id must not exceed %d characters", ref, CacheMaxOverrideIDLength)
v.Mustf(!seen[o.ID], "override %s: duplicate id", ref)
seen[o.ID] = true
}
isCache := o.Action == "" || o.Action == "cache"
v.Mustf(isCache || o.Action == "bypass", "override %s: action invalid (want cache|bypass)", ref)
if isCache {
v.Mustf(o.TTL != "", "override %s: ttl required for action=cache", ref)
if o.TTL != "" {
d, err := time.ParseDuration(o.TTL)
if err != nil {
v.Mustf(false, "override %s: ttl invalid", ref)
} else {
v.Mustf(d >= CacheMinTTL && d <= CacheMaxTTL, "override %s: ttl out of bounds (want %s..%s)", ref, CacheMinTTL, CacheMaxTTL)
}
}
v.Mustf(validCachePolicies[o.Policy], "override %s: policy invalid (want conservative|balanced|aggressive)", ref)
validCacheStaleWindow(v, ref, "staleWhileRevalidate", o.StaleWhileRevalidate)
validCacheStaleWindow(v, ref, "staleIfError", o.StaleIfError)
for _, s := range o.Status {
v.Mustf(s >= 100 && s <= 599, "override %s: status %d invalid (want 100..599)", ref, s)
}
} else {
// bypass: the force-only fields are meaningless and parapet rejects
// them, so reject them here too rather than silently dropping.
v.Mustf(o.TTL == "", "override %s: ttl not valid for action=bypass", ref)
v.Mustf(o.Policy == "", "override %s: policy not valid for action=bypass", ref)
v.Mustf(o.StaleWhileRevalidate == "" && o.StaleIfError == "", "override %s: stale_* not valid for action=bypass", ref)
v.Mustf(len(o.Status) == 0, "override %s: status not valid for action=bypass", ref)
}
v.Mustf(o.Mode == "" || o.Mode == "enforce" || o.Mode == "shadow", "override %s: mode invalid (want enforce|shadow)", ref)
v.Mustf(utf8.RuneCountInString(o.Filter) <= CacheMaxFilterLength, "override %s: filter must not exceed %d characters", ref, CacheMaxFilterLength)
}
}
func validCacheStaleWindow(v *validator.Validator, ref, field, raw string) {
if raw == "" {
return
}
d, err := time.ParseDuration(raw)
if err != nil {
v.Mustf(false, "override %s: %s invalid", ref, field)
return
}
v.Mustf(d >= 0, "override %s: %s must be >= 0", ref, field)
}
type CacheGet struct {
Project string `json:"project" yaml:"project"`
Location string `json:"location" yaml:"location"`
}
func (m *CacheGet) Valid() error {
v := validator.New()
v.Must(m.Project != "", "project required")
v.Must(m.Location != "", "location required")
return WrapValidate(v)
}
// CacheSet upserts the project's cache zone, replacing the whole override set.
// Mirrors parapet's all-or-nothing SetOverrides: one bad override rejects the
// batch and the previous good set stays live.
type CacheSet struct {
Project string `json:"project" yaml:"project"`
Location string `json:"location" yaml:"location"`
Description string `json:"description" yaml:"description"`
Overrides []CacheOverride `json:"overrides" yaml:"overrides"`
}
func (m *CacheSet) Valid() error {
v := validator.New()
v.Must(m.Project != "", "project required")
v.Must(m.Location != "", "location required")
validCacheOverrides(v, m.Overrides)
return WrapValidate(v)
}
type CacheDelete struct {
Project string `json:"project" yaml:"project"`
Location string `json:"location" yaml:"location"`
}
func (m *CacheDelete) Valid() error {
v := validator.New()
v.Must(m.Project != "", "project required")
v.Must(m.Location != "", "location required")
return WrapValidate(v)
}
type CacheList struct {
Project string `json:"project" yaml:"project"`
}
func (m *CacheList) Valid() error {
v := validator.New()
v.Must(m.Project != "", "project required")
return WrapValidate(v)
}
type CacheListResult struct {
Project string `json:"project" yaml:"project"`
Items []*CacheItem `json:"items" yaml:"items"`
}
func (m *CacheListResult) Table() [][]string {
table := [][]string{
{"LOCATION", "OVERRIDES", "STATUS", "AGE"},
}
for _, x := range m.Items {
table = append(table, []string{
x.Location,
strconv.Itoa(len(x.Overrides)),
x.Status.Text(),
age(x.CreatedAt),
})
}
return table
}
type CacheItem struct {
Project string `json:"project" yaml:"project"`
Location string `json:"location" yaml:"location"`
Description string `json:"description" yaml:"description"`
Overrides []CacheOverride `json:"overrides" yaml:"overrides"`
// Status and Action expose the materialization state: Status is Pending
// while the deployer is (un)applying the zone and Success once live; Action
// is Create (set) or Delete (tearing down). Both are read-only.
Status Status `json:"status" yaml:"status"`
Action Action `json:"action" yaml:"action"`
CreatedAt time.Time `json:"createdAt" yaml:"createdAt"`
CreatedBy string `json:"createdBy" yaml:"createdBy"`
}
func (m *CacheItem) Table() [][]string {
table := [][]string{
{"PROJECT", "LOCATION", "OVERRIDES", "STATUS", "AGE"},
{
m.Project,
m.Location,
strconv.Itoa(len(m.Overrides)),
m.Status.Text(),
age(m.CreatedAt),
},
}
return table
}
// CacheMetrics reads a zone's override decision counts
// (parapet_cache_override_total, collected per minute into the apiserver) over a
// time range. Series come per (override, action, result) so the caller can
// chart the applied share and validate a shadow-mode override before it is
// enforced. Reuses WAFMetricsTimeRange.
type CacheMetrics struct {
Project string `json:"project" yaml:"project"`
Location string `json:"location" yaml:"location"`
TimeRange WAFMetricsTimeRange `json:"timeRange" yaml:"timeRange"`
}
func (m *CacheMetrics) Valid() error {
v := validator.New()
v.Must(m.Project != "", "project required")
v.Must(m.Location != "", "location required")
v.Must(validWAFMetricsTimeRange[m.TimeRange], "timeRange invalid")
return WrapValidate(v)
}
// CacheMetricsResult carries decision counts at the (override, action, result)
// grain: Series for the time chart plus the grand Total. OverrideID is the
// short, project-local id, matching Cache.Get so the caller can join a series to
// its override.
type CacheMetricsResult struct {
Series []*CacheMetricsSeries `json:"series" yaml:"series"`
Total float64 `json:"total" yaml:"total"`
}
type CacheMetricsSeries struct {
OverrideID string `json:"overrideId" yaml:"overrideId"`
Action string `json:"action" yaml:"action"` // cache|bypass
Result string `json:"result" yaml:"result"` // applied|shadow|error
Total float64 `json:"total" yaml:"total"` // this series' sum over the range
Points [][2]float64 `json:"points" yaml:"points"` // [unixSeconds, count], time-ordered
}