Skip to content

Commit 8d6e2f0

Browse files
authored
operator: Extend schema validation in LokiStack webhook (grafana#6411)
1 parent 334c394 commit 8d6e2f0

File tree

9 files changed

+374
-242
lines changed

9 files changed

+374
-242
lines changed

operator/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## Main
22

3+
- [6411](https://github.com/grafana/loki/pull/6411) **Red-GV**: Extend schema validation in LokiStack webhook
34
- [6224](https://github.com/grafana/loki/pull/6224) **periklis**: Add support for GRPC over TLS for Loki components
45
- [5952](https://github.com/grafana/loki/pull/5952) **Red-GV**: Add api to change storage schema version
56
- [6363](https://github.com/grafana/loki/pull/6363) **periklis**: Allow optional installation of webhooks (Kind)

operator/api/v1beta1/lokistack_types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,9 @@ type ObjectStorageSecretSpec struct {
369369
Name string `json:"name"`
370370
}
371371

372+
// objectStorageSchemaMap defines the type for mapping a schema version with a date
373+
type objectStorageSchemaMap map[StorageSchemaEffectiveDate]ObjectStorageSchemaVersion
374+
372375
// ObjectStorageSchemaVersion defines the storage schema version which will be
373376
// used with the Loki cluster.
374377
//

operator/api/v1beta1/lokistack_webhook.go

Lines changed: 103 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package v1beta1
22

33
import (
4+
"reflect"
5+
"time"
6+
7+
"github.com/ViaQ/logerr/v2/kverrors"
8+
49
apierrors "k8s.io/apimachinery/pkg/api/errors"
510
"k8s.io/apimachinery/pkg/runtime"
611
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -23,12 +28,17 @@ var _ webhook.Validator = &LokiStack{}
2328

2429
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
2530
func (s *LokiStack) ValidateCreate() error {
26-
return s.validate()
31+
return s.validate(nil)
2732
}
2833

2934
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
30-
func (s *LokiStack) ValidateUpdate(_ runtime.Object) error {
31-
return s.validate()
35+
func (s *LokiStack) ValidateUpdate(old runtime.Object) error {
36+
oldStack, ok := old.(*LokiStack)
37+
if !ok {
38+
t := reflect.TypeOf(old).String()
39+
return apierrors.NewInternalError(kverrors.New("runtime object is incorrect type", "type", t))
40+
}
41+
return s.validate(oldStack)
3242
}
3343

3444
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
@@ -37,12 +47,18 @@ func (s *LokiStack) ValidateDelete() error {
3747
return nil
3848
}
3949

40-
func (s *LokiStack) validate() error {
50+
// ValidateSchemas ensures that the schemas are in a valid format
51+
func (s *ObjectStorageSpec) ValidateSchemas(utcTime time.Time, status LokiStackStorageStatus) field.ErrorList {
4152
var allErrs field.ErrorList
4253

54+
appliedSchemasFound := 0
55+
containsValidStartDate := false
4356
found := make(map[StorageSchemaEffectiveDate]bool)
4457

45-
for i, sc := range s.Spec.Storage.Schemas {
58+
cutoff := utcTime.Add(StorageSchemaUpdateBuffer)
59+
appliedSchemas := buildAppliedSchemaMap(status.Schemas, cutoff)
60+
61+
for i, sc := range s.Schemas {
4662
if found[sc.EffectiveDate] {
4763
allErrs = append(allErrs, field.Invalid(
4864
field.NewPath("Spec").Child("Storage").Child("Schemas").Index(i).Child("EffectiveDate"),
@@ -53,13 +69,79 @@ func (s *LokiStack) validate() error {
5369

5470
found[sc.EffectiveDate] = true
5571

56-
if _, err := sc.EffectiveDate.UTCTime(); err != nil {
72+
date, err := sc.EffectiveDate.UTCTime()
73+
if err != nil {
5774
allErrs = append(allErrs, field.Invalid(
5875
field.NewPath("Spec").Child("Storage").Child("Schemas").Index(i).Child("EffectiveDate"),
5976
sc.EffectiveDate,
6077
ErrParseEffectiveDates.Error(),
6178
))
6279
}
80+
81+
if date.Before(cutoff) {
82+
containsValidStartDate = true
83+
}
84+
85+
// No statuses to compare against or this is a new schema which will be added.
86+
if len(appliedSchemas) == 0 || date.After(cutoff) {
87+
continue
88+
}
89+
90+
appliedSchemaVersion, ok := appliedSchemas[sc.EffectiveDate]
91+
92+
if !ok {
93+
allErrs = append(allErrs, field.Invalid(
94+
field.NewPath("Spec").Child("Storage").Child("Schemas").Index(i),
95+
sc,
96+
ErrSchemaRetroactivelyAdded.Error(),
97+
))
98+
}
99+
100+
if ok && appliedSchemaVersion != sc.Version {
101+
allErrs = append(allErrs, field.Invalid(
102+
field.NewPath("Spec").Child("Storage").Child("Schemas").Index(i),
103+
sc,
104+
ErrSchemaRetroactivelyChanged.Error(),
105+
))
106+
}
107+
108+
appliedSchemasFound++
109+
}
110+
111+
if !containsValidStartDate {
112+
allErrs = append(allErrs, field.Invalid(
113+
field.NewPath("Spec").Child("Storage").Child("Schemas"),
114+
s.Schemas,
115+
ErrMissingValidStartDate.Error(),
116+
))
117+
}
118+
119+
if appliedSchemasFound != len(appliedSchemas) {
120+
allErrs = append(allErrs, field.Invalid(
121+
field.NewPath("Spec").Child("Storage").Child("Schemas"),
122+
s.Schemas,
123+
ErrSchemaRetroactivelyRemoved.Error(),
124+
))
125+
}
126+
127+
if len(allErrs) == 0 {
128+
return nil
129+
}
130+
131+
return allErrs
132+
}
133+
134+
func (s *LokiStack) validate(old *LokiStack) error {
135+
var allErrs field.ErrorList
136+
137+
storageStatus := LokiStackStorageStatus{}
138+
if old != nil {
139+
storageStatus = old.Status.Storage
140+
}
141+
142+
errors := s.Spec.Storage.ValidateSchemas(time.Now().UTC(), storageStatus)
143+
if len(errors) != 0 {
144+
allErrs = append(allErrs, errors...)
63145
}
64146

65147
if len(allErrs) == 0 {
@@ -72,3 +154,18 @@ func (s *LokiStack) validate() error {
72154
allErrs,
73155
)
74156
}
157+
158+
// buildAppliedSchemaMap creates a map of schemas which occur before the given time
159+
func buildAppliedSchemaMap(schemas []ObjectStorageSchema, effectiveDate time.Time) objectStorageSchemaMap {
160+
appliedMap := objectStorageSchemaMap{}
161+
162+
for _, schema := range schemas {
163+
date, err := schema.EffectiveDate.UTCTime()
164+
165+
if err == nil && date.Before(effectiveDate) {
166+
appliedMap[schema.EffectiveDate] = schema.Version
167+
}
168+
}
169+
170+
return appliedMap
171+
}

operator/api/v1beta1/lokistack_webhook_test.go

Lines changed: 192 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ var ltt = []struct {
1818
err *apierrors.StatusError
1919
}{
2020
{
21-
desc: "valid spec",
21+
desc: "valid spec - no status",
2222
spec: v1beta1.LokiStack{
2323
Spec: v1beta1.LokiStackSpec{
2424
Storage: v1beta1.ObjectStorageSpec{
@@ -36,6 +36,39 @@ var ltt = []struct {
3636
},
3737
},
3838
},
39+
{
40+
desc: "valid spec - with status",
41+
spec: v1beta1.LokiStack{
42+
Spec: v1beta1.LokiStackSpec{
43+
Storage: v1beta1.ObjectStorageSpec{
44+
Schemas: []v1beta1.ObjectStorageSchema{
45+
{
46+
Version: v1beta1.ObjectStorageSchemaV11,
47+
EffectiveDate: "2020-10-11",
48+
},
49+
{
50+
Version: v1beta1.ObjectStorageSchemaV12,
51+
EffectiveDate: "2020-10-13",
52+
},
53+
},
54+
},
55+
},
56+
Status: v1beta1.LokiStackStatus{
57+
Storage: v1beta1.LokiStackStorageStatus{
58+
Schemas: []v1beta1.ObjectStorageSchema{
59+
{
60+
Version: v1beta1.ObjectStorageSchemaV11,
61+
EffectiveDate: "2020-10-11",
62+
},
63+
{
64+
Version: v1beta1.ObjectStorageSchemaV12,
65+
EffectiveDate: "2020-10-13",
66+
},
67+
},
68+
},
69+
},
70+
},
71+
},
3972
{
4073
desc: "not unique schema effective dates",
4174
spec: v1beta1.LokiStack{
@@ -92,6 +125,164 @@ var ltt = []struct {
92125
},
93126
),
94127
},
128+
{
129+
desc: "missing valid starting date",
130+
spec: v1beta1.LokiStack{
131+
Spec: v1beta1.LokiStackSpec{
132+
Storage: v1beta1.ObjectStorageSpec{
133+
Schemas: []v1beta1.ObjectStorageSchema{
134+
{
135+
Version: v1beta1.ObjectStorageSchemaV11,
136+
EffectiveDate: "9000-10-10",
137+
},
138+
},
139+
},
140+
},
141+
},
142+
err: apierrors.NewInvalid(
143+
schema.GroupKind{Group: "loki.grafana.com", Kind: "LokiStack"},
144+
"testing-stack",
145+
field.ErrorList{
146+
field.Invalid(
147+
field.NewPath("Spec").Child("Storage").Child("Schemas"),
148+
[]v1beta1.ObjectStorageSchema{
149+
{
150+
Version: v1beta1.ObjectStorageSchemaV11,
151+
EffectiveDate: "9000-10-10",
152+
},
153+
},
154+
v1beta1.ErrMissingValidStartDate.Error(),
155+
),
156+
},
157+
),
158+
},
159+
{
160+
desc: "retroactively adding schema",
161+
spec: v1beta1.LokiStack{
162+
Spec: v1beta1.LokiStackSpec{
163+
Storage: v1beta1.ObjectStorageSpec{
164+
Schemas: []v1beta1.ObjectStorageSchema{
165+
{
166+
Version: v1beta1.ObjectStorageSchemaV11,
167+
EffectiveDate: "2020-10-11",
168+
},
169+
{
170+
Version: v1beta1.ObjectStorageSchemaV12,
171+
EffectiveDate: "2020-10-14",
172+
},
173+
},
174+
},
175+
},
176+
Status: v1beta1.LokiStackStatus{
177+
Storage: v1beta1.LokiStackStorageStatus{
178+
Schemas: []v1beta1.ObjectStorageSchema{
179+
{
180+
Version: v1beta1.ObjectStorageSchemaV11,
181+
EffectiveDate: "2020-10-11",
182+
},
183+
},
184+
},
185+
},
186+
},
187+
err: apierrors.NewInvalid(
188+
schema.GroupKind{Group: "loki.grafana.com", Kind: "LokiStack"},
189+
"testing-stack",
190+
field.ErrorList{
191+
field.Invalid(
192+
field.NewPath("Spec").Child("Storage").Child("Schemas"),
193+
v1beta1.ObjectStorageSchema{
194+
Version: v1beta1.ObjectStorageSchemaV12,
195+
EffectiveDate: "2020-10-14",
196+
},
197+
v1beta1.ErrSchemaRetroactivelyAdded.Error(),
198+
),
199+
},
200+
),
201+
},
202+
{
203+
desc: "retroactively removing schema",
204+
spec: v1beta1.LokiStack{
205+
Spec: v1beta1.LokiStackSpec{
206+
Storage: v1beta1.ObjectStorageSpec{
207+
Schemas: []v1beta1.ObjectStorageSchema{
208+
{
209+
Version: v1beta1.ObjectStorageSchemaV11,
210+
EffectiveDate: "2020-10-11",
211+
},
212+
},
213+
},
214+
},
215+
Status: v1beta1.LokiStackStatus{
216+
Storage: v1beta1.LokiStackStorageStatus{
217+
Schemas: []v1beta1.ObjectStorageSchema{
218+
{
219+
Version: v1beta1.ObjectStorageSchemaV11,
220+
EffectiveDate: "2020-10-11",
221+
},
222+
{
223+
Version: v1beta1.ObjectStorageSchemaV12,
224+
EffectiveDate: "2020-10-14",
225+
},
226+
},
227+
},
228+
},
229+
},
230+
err: apierrors.NewInvalid(
231+
schema.GroupKind{Group: "loki.grafana.com", Kind: "LokiStack"},
232+
"testing-stack",
233+
field.ErrorList{
234+
field.Invalid(
235+
field.NewPath("Spec").Child("Storage").Child("Schemas"),
236+
[]v1beta1.ObjectStorageSchema{
237+
{
238+
Version: v1beta1.ObjectStorageSchemaV11,
239+
EffectiveDate: "2020-10-11",
240+
},
241+
},
242+
v1beta1.ErrSchemaRetroactivelyRemoved.Error(),
243+
),
244+
},
245+
),
246+
},
247+
{
248+
desc: "retroactively changing schema",
249+
spec: v1beta1.LokiStack{
250+
Spec: v1beta1.LokiStackSpec{
251+
Storage: v1beta1.ObjectStorageSpec{
252+
Schemas: []v1beta1.ObjectStorageSchema{
253+
{
254+
Version: v1beta1.ObjectStorageSchemaV12,
255+
EffectiveDate: "2020-10-11",
256+
},
257+
},
258+
},
259+
},
260+
Status: v1beta1.LokiStackStatus{
261+
Storage: v1beta1.LokiStackStorageStatus{
262+
Schemas: []v1beta1.ObjectStorageSchema{
263+
{
264+
Version: v1beta1.ObjectStorageSchemaV11,
265+
EffectiveDate: "2020-10-11",
266+
},
267+
},
268+
},
269+
},
270+
},
271+
err: apierrors.NewInvalid(
272+
schema.GroupKind{Group: "loki.grafana.com", Kind: "LokiStack"},
273+
"testing-stack",
274+
field.ErrorList{
275+
field.Invalid(
276+
field.NewPath("Spec").Child("Storage").Child("Schemas"),
277+
v1beta1.ObjectStorageSchema{
278+
Version: v1beta1.ObjectStorageSchemaV12,
279+
EffectiveDate: "2020-10-11",
280+
},
281+
v1beta1.ErrSchemaRetroactivelyChanged.Error(),
282+
),
283+
},
284+
),
285+
},
95286
}
96287

97288
func TestLokiStackValidationWebhook_ValidateCreate(t *testing.T) {

0 commit comments

Comments
 (0)