dra api: implement semver attribute value type

This adds support for semantic version comparison to the CEL support in the
"named resources" structured parameter model. For example, it can be used to
check that an instance supports a certain API level.

To minimize the risk, the new "semver" type is only defined in the CEL
environment for DRA expressions, not in the base library. See
https://github.com/kubernetes/kubernetes/pull/123664 for a PR which
adds it to the base library.

Validation of semver strings is done with the regular expression from
semver.org. The actual evaluation at runtime then uses semver/v4.
This commit is contained in:
Patrick Ohly 2024-03-04 09:13:19 +01:00
parent 234dc1f63d
commit 42ee56f093
25 changed files with 891 additions and 194 deletions

View File

@ -57,6 +57,7 @@ API rule violation: names_match,k8s.io/api/resource/v1alpha2,NamedResourcesAttri
API rule violation: names_match,k8s.io/api/resource/v1alpha2,NamedResourcesAttributeValue,QuantityValue API rule violation: names_match,k8s.io/api/resource/v1alpha2,NamedResourcesAttributeValue,QuantityValue
API rule violation: names_match,k8s.io/api/resource/v1alpha2,NamedResourcesAttributeValue,StringSliceValue API rule violation: names_match,k8s.io/api/resource/v1alpha2,NamedResourcesAttributeValue,StringSliceValue
API rule violation: names_match,k8s.io/api/resource/v1alpha2,NamedResourcesAttributeValue,StringValue API rule violation: names_match,k8s.io/api/resource/v1alpha2,NamedResourcesAttributeValue,StringValue
API rule violation: names_match,k8s.io/api/resource/v1alpha2,NamedResourcesAttributeValue,VersionValue
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaProps,Ref API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaProps,Ref
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaProps,Schema API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaProps,Schema
API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaProps,XEmbeddedResource API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaProps,XEmbeddedResource

View File

@ -14900,6 +14900,10 @@
"stringSlice": { "stringSlice": {
"$ref": "#/definitions/io.k8s.api.resource.v1alpha2.NamedResourcesStringSlice", "$ref": "#/definitions/io.k8s.api.resource.v1alpha2.NamedResourcesStringSlice",
"description": "StringSliceValue is an array of strings." "description": "StringSliceValue is an array of strings."
},
"version": {
"description": "VersionValue is a semantic version according to semver.org spec 2.0.0.",
"type": "string"
} }
}, },
"required": [ "required": [

View File

@ -228,6 +228,10 @@
} }
], ],
"description": "StringSliceValue is an array of strings." "description": "StringSliceValue is an array of strings."
},
"version": {
"description": "VersionValue is a semantic version according to semver.org spec 2.0.0.",
"type": "string"
} }
}, },
"required": [ "required": [

View File

@ -59,7 +59,8 @@ type NamedResourcesAttributeValue struct {
StringValue *string StringValue *string
// StringSliceValue is an array of strings. // StringSliceValue is an array of strings.
StringSliceValue *NamedResourcesStringSlice StringSliceValue *NamedResourcesStringSlice
// TODO: VersionValue *SemVersion // VersionValue is a semantic version according to semver.org spec 2.0.0.
VersionValue *string
} }
// NamedResourcesIntSlice contains a slice of 64-bit integers. // NamedResourcesIntSlice contains a slice of 64-bit integers.
@ -74,17 +75,6 @@ type NamedResourcesStringSlice struct {
Strings []string Strings []string
} }
// TODO
//
// A wrapper around https://pkg.go.dev/github.com/blang/semver/v4#Version which
// is encoded as a string. During decoding, it validates that the string
// can be parsed using tolerant parsing (currently trims spaces, removes a "v" prefix,
// adds a 0 patch number to versions with only major and minor components specified,
// and removes leading 0s).
// type SemVersion struct {
// semver.Version
//}
// NamedResourcesRequest is used in ResourceRequestModel. // NamedResourcesRequest is used in ResourceRequestModel.
type NamedResourcesRequest struct { type NamedResourcesRequest struct {
// Selector is a CEL expression which must evaluate to true if a // Selector is a CEL expression which must evaluate to true if a

View File

@ -18,6 +18,7 @@ package validation
import ( import (
"fmt" "fmt"
"regexp"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/validation/field"
@ -63,6 +64,27 @@ func validateInstances(instances []resource.NamedResourcesInstance, fldPath *fie
return allErrs return allErrs
} }
var (
numericIdentifier = `(0|[1-9]\d*)`
preReleaseIdentifier = `(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)`
buildIdentifier = `[0-9a-zA-Z-]+`
semverRe = regexp.MustCompile(`^` +
// dot-separated version segments (e.g. 1.2.3)
numericIdentifier + `\.` + numericIdentifier + `\.` + numericIdentifier +
// optional dot-separated prerelease segments (e.g. -alpha.PRERELEASE.1)
`(-` + preReleaseIdentifier + `(\.` + preReleaseIdentifier + `)*)?` +
// optional dot-separated build identifier segments (e.g. +build.id.20240305)
`(\+` + buildIdentifier + `(\.` + buildIdentifier + `)*)?` +
`$`)
)
func validateAttributes(attributes []resource.NamedResourcesAttribute, fldPath *field.Path) field.ErrorList { func validateAttributes(attributes []resource.NamedResourcesAttribute, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList var allErrs field.ErrorList
attributeNames := sets.New[string]() attributeNames := sets.New[string]()
@ -95,7 +117,12 @@ func validateAttributes(attributes []resource.NamedResourcesAttribute, fldPath *
if attribute.StringSliceValue != nil { if attribute.StringSliceValue != nil {
entries.Insert("stringSlice") entries.Insert("stringSlice")
} }
// TODO: VersionValue if attribute.VersionValue != nil {
entries.Insert("version")
if !semverRe.MatchString(*attribute.VersionValue) {
allErrs = append(allErrs, field.Invalid(idxPath.Child("version"), *attribute.VersionValue, "must be a string compatible with semver.org spec 2.0.0"))
}
}
switch len(entries) { switch len(entries) {
case 0: case 0:

View File

@ -75,7 +75,44 @@ func TestValidateResources(t *testing.T) {
"string-slice": { "string-slice": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{StringSliceValue: &resourceapi.NamedResourcesStringSlice{Strings: []string{"hello"}}}}}}}), resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{StringSliceValue: &resourceapi.NamedResourcesStringSlice{Strings: []string{"hello"}}}}}}}),
}, },
// TODO: semver "version-okay": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.0")}}}}}),
},
"version-beta": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.0-beta")}}}}}),
},
"version-beta-1": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.0-beta.1")}}}}}),
},
"version-build": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.0+build")}}}}}),
},
"version-build-1": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.0+build.1")}}}}}),
},
"version-beta-1-build-1": {
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.0-beta.1+build.1")}}}}}),
},
"version-bad": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("attributes").Index(0).Child("version"), "1.0", "must be a string compatible with semver.org spec 2.0.0")},
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0")}}}}}),
},
"version-bad-leading-zeros": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("attributes").Index(0).Child("version"), "01.0.0", "must be a string compatible with semver.org spec 2.0.0")},
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("01.0.0")}}}}}),
},
"version-bad-leading-zeros-middle": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("attributes").Index(0).Child("version"), "1.00.0", "must be a string compatible with semver.org spec 2.0.0")},
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.00.0")}}}}}),
},
"version-bad-leading-zeros-end": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("attributes").Index(0).Child("version"), "1.0.00", "must be a string compatible with semver.org spec 2.0.0")},
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To("1.0.00")}}}}}),
},
"version-bad-spaces": {
wantFailures: field.ErrorList{field.Invalid(field.NewPath("instances").Index(0).Child("attributes").Index(0).Child("version"), " 1.0.0 ", "must be a string compatible with semver.org spec 2.0.0")},
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName, NamedResourcesAttributeValue: resourceapi.NamedResourcesAttributeValue{VersionValue: ptr.To(" 1.0.0 ")}}}}}),
},
"empty-attribute": { "empty-attribute": {
wantFailures: field.ErrorList{field.Required(field.NewPath("instances").Index(0).Child("attributes").Index(0), "exactly one value must be set")}, wantFailures: field.ErrorList{field.Required(field.NewPath("instances").Index(0).Child("attributes").Index(0), "exactly one value must be set")},
resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName}}}}), resources: testResources([]resourceapi.NamedResourcesInstance{{Name: goodName, Attributes: []resourceapi.NamedResourcesAttribute{{Name: goodName}}}}),
@ -132,7 +169,9 @@ func TestValidateSelector(t *testing.T) {
"stringslice": { "stringslice": {
selector: `attributes.stringslice["name"].isSorted()`, selector: `attributes.stringslice["name"].isSorted()`,
}, },
// TODO: semver "version": {
selector: `attributes.version["name"].isGreaterThan(semver("1.0.0"))`,
},
} }
for name, scenario := range scenarios { for name, scenario := range scenarios {

View File

@ -679,6 +679,7 @@ func autoConvert_v1alpha2_NamedResourcesAttributeValue_To_resource_NamedResource
out.IntSliceValue = (*resource.NamedResourcesIntSlice)(unsafe.Pointer(in.IntSliceValue)) out.IntSliceValue = (*resource.NamedResourcesIntSlice)(unsafe.Pointer(in.IntSliceValue))
out.StringValue = (*string)(unsafe.Pointer(in.StringValue)) out.StringValue = (*string)(unsafe.Pointer(in.StringValue))
out.StringSliceValue = (*resource.NamedResourcesStringSlice)(unsafe.Pointer(in.StringSliceValue)) out.StringSliceValue = (*resource.NamedResourcesStringSlice)(unsafe.Pointer(in.StringSliceValue))
out.VersionValue = (*string)(unsafe.Pointer(in.VersionValue))
return nil return nil
} }
@ -694,6 +695,7 @@ func autoConvert_resource_NamedResourcesAttributeValue_To_v1alpha2_NamedResource
out.IntSliceValue = (*v1alpha2.NamedResourcesIntSlice)(unsafe.Pointer(in.IntSliceValue)) out.IntSliceValue = (*v1alpha2.NamedResourcesIntSlice)(unsafe.Pointer(in.IntSliceValue))
out.StringValue = (*string)(unsafe.Pointer(in.StringValue)) out.StringValue = (*string)(unsafe.Pointer(in.StringValue))
out.StringSliceValue = (*v1alpha2.NamedResourcesStringSlice)(unsafe.Pointer(in.StringSliceValue)) out.StringSliceValue = (*v1alpha2.NamedResourcesStringSlice)(unsafe.Pointer(in.StringSliceValue))
out.VersionValue = (*string)(unsafe.Pointer(in.VersionValue))
return nil return nil
} }

View File

@ -187,6 +187,11 @@ func (in *NamedResourcesAttributeValue) DeepCopyInto(out *NamedResourcesAttribut
*out = new(NamedResourcesStringSlice) *out = new(NamedResourcesStringSlice)
(*in).DeepCopyInto(*out) (*in).DeepCopyInto(*out)
} }
if in.VersionValue != nil {
in, out := &in.VersionValue, &out.VersionValue
*out = new(string)
**out = **in
}
return return
} }

View File

@ -44907,6 +44907,13 @@ func schema_k8sio_api_resource_v1alpha2_NamedResourcesAttribute(ref common.Refer
Ref: ref("k8s.io/api/resource/v1alpha2.NamedResourcesStringSlice"), Ref: ref("k8s.io/api/resource/v1alpha2.NamedResourcesStringSlice"),
}, },
}, },
"version": {
SchemaProps: spec.SchemaProps{
Description: "VersionValue is a semantic version according to semver.org spec 2.0.0.",
Type: []string{"string"},
Format: "",
},
},
}, },
Required: []string{"name"}, Required: []string{"name"},
}, },
@ -44962,6 +44969,13 @@ func schema_k8sio_api_resource_v1alpha2_NamedResourcesAttributeValue(ref common.
Ref: ref("k8s.io/api/resource/v1alpha2.NamedResourcesStringSlice"), Ref: ref("k8s.io/api/resource/v1alpha2.NamedResourcesStringSlice"),
}, },
}, },
"version": {
SchemaProps: spec.SchemaProps{
Description: "VersionValue is a semantic version according to semver.org spec 2.0.0.",
Type: []string{"string"},
Format: "",
},
},
}, },
}, },
}, },

View File

@ -1331,147 +1331,149 @@ func init() {
} }
var fileDescriptor_4312f5b44a31ec02 = []byte{ var fileDescriptor_4312f5b44a31ec02 = []byte{
// 2231 bytes of a gzipped FileDescriptorProto // 2257 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x1a, 0x4b, 0x6f, 0x1b, 0xc7, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x3a, 0xcd, 0x6f, 0x1b, 0xc7,
0xd9, 0x4b, 0xd2, 0x16, 0xf5, 0xc9, 0xa2, 0xa4, 0xb5, 0x64, 0xd1, 0x8e, 0x42, 0x2a, 0x8b, 0x16, 0xf5, 0x5e, 0x92, 0xb6, 0xa8, 0x27, 0x8b, 0x92, 0xd6, 0x92, 0x4d, 0x3b, 0x0a, 0xa9, 0x2c, 0x7e,
0x15, 0x50, 0x9b, 0x8c, 0xe5, 0xc4, 0x31, 0xd2, 0xb4, 0x80, 0xd7, 0x8a, 0x5d, 0xa1, 0x89, 0xa3, 0x3f, 0x54, 0x68, 0x6d, 0x32, 0x96, 0x13, 0xc7, 0x48, 0xd3, 0x02, 0x5e, 0x2b, 0x76, 0x85, 0x26,
0x0c, 0x63, 0x27, 0x4e, 0x5f, 0x59, 0x71, 0xc7, 0xd2, 0xd6, 0xe4, 0x2e, 0xbd, 0x33, 0x54, 0x6c, 0x8e, 0x32, 0x8c, 0x95, 0x38, 0xfd, 0xca, 0x8a, 0x3b, 0x96, 0xb6, 0x26, 0x77, 0xe9, 0x9d, 0xa1,
0xf4, 0x62, 0x14, 0x7d, 0x5d, 0x0a, 0xa4, 0x68, 0x51, 0xb4, 0xa7, 0x9e, 0x8a, 0xa2, 0x97, 0x5e, 0x62, 0xa3, 0x17, 0xa3, 0xe8, 0xd7, 0xa5, 0x40, 0x8a, 0x16, 0x45, 0x7b, 0xea, 0xa9, 0x28, 0x7a,
0xda, 0x7b, 0x0f, 0x41, 0x1b, 0x03, 0xbd, 0x38, 0x68, 0x81, 0x06, 0x3d, 0x10, 0x35, 0xfb, 0x13, 0xe9, 0xa5, 0xfd, 0x0f, 0x82, 0x36, 0x06, 0x7a, 0x71, 0xd0, 0x02, 0x0d, 0x7a, 0x20, 0x6a, 0xf6,
0x7a, 0xcb, 0xa9, 0x98, 0xc7, 0x3e, 0x66, 0xb9, 0x4b, 0x71, 0xd9, 0x46, 0x48, 0x4e, 0xd2, 0xce, 0xd8, 0x63, 0x6f, 0x39, 0x15, 0xf3, 0xb1, 0x1f, 0xb3, 0xdc, 0xa5, 0xb8, 0x6c, 0x23, 0x24, 0x27,
0x7c, 0xef, 0xef, 0x9b, 0xef, 0x31, 0x43, 0x38, 0x77, 0xf7, 0x32, 0x69, 0x38, 0x5e, 0xd3, 0xea, 0x72, 0x66, 0xde, 0xf7, 0x7b, 0xf3, 0xde, 0x9b, 0x99, 0x85, 0xf3, 0x77, 0xaf, 0x90, 0x86, 0xe3,
0x39, 0x4d, 0x1f, 0x13, 0xaf, 0xef, 0xb7, 0x71, 0xf3, 0xe0, 0x82, 0xd5, 0xe9, 0xed, 0x5b, 0x9b, 0x35, 0xad, 0x9e, 0xd3, 0xf4, 0x31, 0xf1, 0xfa, 0x7e, 0x1b, 0x37, 0x0f, 0x2e, 0x5a, 0x9d, 0xde,
0xcd, 0x3d, 0xec, 0x62, 0xdf, 0xa2, 0xd8, 0x6e, 0xf4, 0x7c, 0x8f, 0x7a, 0xfa, 0x9a, 0x80, 0x6e, 0xbe, 0xb5, 0xd1, 0xdc, 0xc3, 0x2e, 0xf6, 0x2d, 0x8a, 0xed, 0x46, 0xcf, 0xf7, 0xa8, 0xa7, 0xaf,
0x58, 0x3d, 0xa7, 0x11, 0x40, 0x37, 0x02, 0xe8, 0xb3, 0xe7, 0xf7, 0x1c, 0xba, 0xdf, 0xdf, 0x6d, 0x0a, 0xe8, 0x86, 0xd5, 0x73, 0x1a, 0x01, 0x74, 0x23, 0x80, 0x3e, 0x77, 0x61, 0xcf, 0xa1, 0xfb,
0xb4, 0xbd, 0x6e, 0x73, 0xcf, 0xdb, 0xf3, 0x9a, 0x1c, 0x69, 0xb7, 0x7f, 0x87, 0x7f, 0xf1, 0x0f, 0xfd, 0xdd, 0x46, 0xdb, 0xeb, 0x36, 0xf7, 0xbc, 0x3d, 0xaf, 0xc9, 0x91, 0x76, 0xfb, 0x77, 0xf8,
0xfe, 0x9f, 0x20, 0x76, 0xd6, 0x88, 0xb1, 0x6e, 0x7b, 0x3e, 0x63, 0x9b, 0x64, 0x78, 0xf6, 0xb9, 0x88, 0x0f, 0xf8, 0x3f, 0x41, 0xec, 0x9c, 0x11, 0x63, 0xdd, 0xf6, 0x7c, 0xc6, 0x36, 0xc9, 0xf0,
0x08, 0xa6, 0x6b, 0xb5, 0xf7, 0x1d, 0x17, 0xfb, 0x0f, 0x9a, 0xbd, 0xbb, 0x7b, 0xaa, 0xbc, 0x79, 0xdc, 0x73, 0x11, 0x4c, 0xd7, 0x6a, 0xef, 0x3b, 0x2e, 0xf6, 0x1f, 0x34, 0x7b, 0x77, 0xf7, 0x54,
0xb0, 0x48, 0xb3, 0x8b, 0xa9, 0x95, 0xc6, 0xab, 0x99, 0x85, 0xe5, 0xf7, 0x5d, 0xea, 0x74, 0x47, 0x79, 0xf3, 0x60, 0x91, 0x66, 0x17, 0x53, 0x2b, 0x8d, 0x57, 0x33, 0x0b, 0xcb, 0xef, 0xbb, 0xd4,
0xd9, 0x5c, 0x3a, 0x0c, 0x81, 0xb4, 0xf7, 0x71, 0xd7, 0x4a, 0xe2, 0x19, 0xbf, 0x2c, 0xc0, 0xe2, 0xe9, 0x8e, 0xb2, 0xb9, 0x7c, 0x18, 0x02, 0x69, 0xef, 0xe3, 0xae, 0x95, 0xc4, 0x33, 0x7e, 0x51,
0x95, 0x4e, 0xc7, 0x6b, 0x5b, 0xd4, 0xf1, 0x5c, 0x84, 0x49, 0xbf, 0x43, 0x75, 0x0f, 0x16, 0x02, 0x80, 0xc5, 0xab, 0x9d, 0x8e, 0xd7, 0xb6, 0xa8, 0xe3, 0xb9, 0x08, 0x93, 0x7e, 0x87, 0xea, 0x1e,
0x7d, 0xbe, 0x6a, 0xb9, 0x76, 0x07, 0x93, 0xaa, 0xb6, 0x5e, 0xdc, 0x98, 0xdb, 0x3c, 0xd7, 0x18, 0x2c, 0x04, 0xfa, 0x7c, 0xc5, 0x72, 0xed, 0x0e, 0x26, 0x55, 0x6d, 0xad, 0xb8, 0x3e, 0xb7, 0x71,
0x67, 0xf4, 0x06, 0x52, 0x90, 0xcc, 0xd5, 0x47, 0x83, 0xfa, 0xb1, 0xe1, 0xa0, 0xbe, 0xa0, 0xae, 0xbe, 0x31, 0xce, 0xe8, 0x0d, 0xa4, 0x20, 0x99, 0x67, 0x1e, 0x0d, 0xea, 0xc7, 0x86, 0x83, 0xfa,
0x13, 0x94, 0xa4, 0xae, 0xef, 0xc2, 0xa2, 0x75, 0x60, 0x39, 0x1d, 0x6b, 0xb7, 0x83, 0x5f, 0x73, 0x82, 0x3a, 0x4f, 0x50, 0x92, 0xba, 0xbe, 0x0b, 0x8b, 0xd6, 0x81, 0xe5, 0x74, 0xac, 0xdd, 0x0e,
0x6f, 0x78, 0x36, 0x26, 0xd5, 0xc2, 0xba, 0xb6, 0x31, 0xb7, 0xb9, 0x1e, 0xe7, 0xc8, 0x3c, 0xd3, 0x7e, 0xcd, 0xbd, 0xe9, 0xd9, 0x98, 0x54, 0x0b, 0x6b, 0xda, 0xfa, 0xdc, 0xc6, 0x5a, 0x9c, 0x23,
0x38, 0xb8, 0xd0, 0x60, 0x00, 0x2d, 0xdc, 0xc1, 0x6d, 0xea, 0xf9, 0xe6, 0xf2, 0x70, 0x50, 0x5f, 0xf3, 0x4c, 0xe3, 0xe0, 0x62, 0x83, 0x01, 0xb4, 0x70, 0x07, 0xb7, 0xa9, 0xe7, 0x9b, 0xcb, 0xc3,
0xbc, 0x92, 0xc0, 0x46, 0x23, 0xf4, 0xf4, 0x26, 0xcc, 0x92, 0x7d, 0xcb, 0xc7, 0x6c, 0xad, 0x5a, 0x41, 0x7d, 0xf1, 0x6a, 0x02, 0x1b, 0x8d, 0xd0, 0xd3, 0x9b, 0x30, 0x4b, 0xf6, 0x2d, 0x1f, 0xb3,
0x5c, 0xd7, 0x36, 0xca, 0xe6, 0x92, 0x14, 0x70, 0xb6, 0x15, 0x6c, 0xa0, 0x08, 0xc6, 0xf8, 0xa9, 0xb9, 0x6a, 0x71, 0x4d, 0x5b, 0x2f, 0x9b, 0x4b, 0x52, 0xc0, 0xd9, 0x56, 0xb0, 0x80, 0x22, 0x18,
0x06, 0x2b, 0x49, 0xd3, 0xbc, 0xea, 0xd9, 0xb8, 0xa3, 0xdf, 0x87, 0x8a, 0x6b, 0x75, 0xb1, 0x1d, 0xe3, 0x27, 0x1a, 0xac, 0x24, 0x4d, 0xf3, 0xaa, 0x67, 0xe3, 0x8e, 0x7e, 0x1f, 0x2a, 0xae, 0xd5,
0xe8, 0xc5, 0xcc, 0xc3, 0x84, 0x7d, 0x69, 0xbc, 0x79, 0x6e, 0x28, 0x38, 0x49, 0xd2, 0xa6, 0x3e, 0xc5, 0x76, 0xa0, 0x17, 0x33, 0x0f, 0x13, 0xf6, 0xa5, 0xf1, 0xe6, 0xb9, 0xa9, 0xe0, 0x24, 0x49,
0x1c, 0xd4, 0x2b, 0x2a, 0x0c, 0x4a, 0xf0, 0x31, 0x7e, 0x5f, 0x80, 0xd3, 0x5b, 0xbe, 0x73, 0x80, 0x9b, 0xfa, 0x70, 0x50, 0xaf, 0xa8, 0x30, 0x28, 0xc1, 0xc7, 0xf8, 0x5d, 0x01, 0x4e, 0x6f, 0xfa,
0xfd, 0x11, 0xa7, 0xfd, 0x58, 0x83, 0xd5, 0x03, 0xec, 0xda, 0x9e, 0x8f, 0xf0, 0xbd, 0x3e, 0x26, 0xce, 0x01, 0xf6, 0x47, 0x9c, 0xf6, 0x23, 0x0d, 0xce, 0x1c, 0x60, 0xd7, 0xf6, 0x7c, 0x84, 0xef,
0x74, 0xc7, 0xf2, 0xad, 0x2e, 0xa6, 0xd8, 0x0f, 0xc4, 0x3b, 0x1f, 0x13, 0x2f, 0x0c, 0x92, 0x46, 0xf5, 0x31, 0xa1, 0xdb, 0x96, 0x6f, 0x75, 0x31, 0xc5, 0x7e, 0x20, 0xde, 0x85, 0x98, 0x78, 0x61,
0xef, 0xee, 0x5e, 0x43, 0x06, 0x49, 0x03, 0x59, 0xef, 0xbe, 0x7c, 0x9f, 0x62, 0x97, 0x38, 0x9e, 0x90, 0x34, 0x7a, 0x77, 0xf7, 0x1a, 0x32, 0x48, 0x1a, 0xc8, 0x7a, 0xf7, 0xe5, 0xfb, 0x14, 0xbb,
0x6b, 0xd6, 0xa5, 0x75, 0x56, 0x6f, 0xa5, 0x53, 0x45, 0x59, 0xec, 0x98, 0x28, 0x2b, 0x56, 0x9a, 0xc4, 0xf1, 0x5c, 0xb3, 0x2e, 0xad, 0x73, 0x66, 0x27, 0x9d, 0x2a, 0xca, 0x62, 0xc7, 0x44, 0x59,
0xe5, 0xa4, 0x53, 0x2f, 0x8e, 0xb7, 0x53, 0xaa, 0xd1, 0xcd, 0xa7, 0xa5, 0x38, 0xe9, 0x3e, 0x41, 0xb1, 0xd2, 0x2c, 0x27, 0x9d, 0x7a, 0x69, 0xbc, 0x9d, 0x52, 0x8d, 0x6e, 0x3e, 0x2d, 0xc5, 0x49,
0xe9, 0x0c, 0x8d, 0x5f, 0x14, 0xa0, 0x22, 0x0c, 0x26, 0xc5, 0x24, 0xfa, 0x26, 0x80, 0xcd, 0x57, 0xf7, 0x09, 0x4a, 0x67, 0x68, 0xfc, 0xbc, 0x00, 0x15, 0x61, 0x30, 0x29, 0x26, 0xd1, 0x37, 0x00,
0x98, 0xad, 0xb9, 0x69, 0x66, 0x4d, 0x5d, 0x12, 0x87, 0xad, 0x70, 0x07, 0xc5, 0xa0, 0x74, 0x02, 0x6c, 0x3e, 0xc3, 0x6c, 0xcd, 0x4d, 0x33, 0x6b, 0xea, 0x92, 0x38, 0x6c, 0x86, 0x2b, 0x28, 0x06,
0x8b, 0x42, 0xd9, 0x98, 0x51, 0x0b, 0xd3, 0x18, 0xb5, 0x2a, 0x19, 0x2d, 0xde, 0x4a, 0x90, 0x43, 0xa5, 0x13, 0x58, 0x14, 0xca, 0xc6, 0x8c, 0x5a, 0x98, 0xc6, 0xa8, 0x55, 0xc9, 0x68, 0x71, 0x27,
0x23, 0x0c, 0xf4, 0xaf, 0x43, 0xd9, 0x97, 0x42, 0x57, 0x8b, 0xfc, 0xfc, 0x9d, 0x9f, 0xec, 0xfc, 0x41, 0x0e, 0x8d, 0x30, 0xd0, 0xbf, 0x06, 0x65, 0x5f, 0x0a, 0x5d, 0x2d, 0xf2, 0xfd, 0x77, 0x61,
0x49, 0x55, 0xcd, 0x45, 0xc9, 0xac, 0x1c, 0xe8, 0x8e, 0x42, 0x82, 0x86, 0x09, 0xb5, 0xf1, 0xf1, 0xb2, 0xfd, 0x27, 0x55, 0x35, 0x17, 0x25, 0xb3, 0x72, 0xa0, 0x3b, 0x0a, 0x09, 0x1a, 0x26, 0xd4,
0xa8, 0xaf, 0x43, 0xc9, 0x8d, 0x2c, 0x74, 0x52, 0xd2, 0x2a, 0x71, 0xdb, 0xf0, 0x1d, 0xe3, 0x2f, 0xc6, 0xc7, 0xa3, 0xbe, 0x06, 0x25, 0x37, 0xb2, 0xd0, 0x49, 0x49, 0xab, 0xc4, 0x6d, 0xc3, 0x57,
0x1a, 0xac, 0x26, 0x88, 0x50, 0xea, 0x3b, 0xbb, 0x7d, 0x8a, 0x0f, 0xc7, 0x66, 0x51, 0x52, 0xb1, 0x8c, 0x3f, 0x69, 0x70, 0x26, 0x41, 0x84, 0x52, 0xdf, 0xd9, 0xed, 0x53, 0x7c, 0x38, 0x36, 0x8b,
0x02, 0xf8, 0x5b, 0x56, 0xa7, 0x8f, 0xa5, 0x49, 0x5f, 0xcc, 0x75, 0x8c, 0x14, 0x0a, 0xe6, 0xe7, 0x92, 0x8a, 0x15, 0xc0, 0xef, 0x58, 0x9d, 0x3e, 0x96, 0x26, 0x7d, 0x31, 0xd7, 0x36, 0x52, 0x28,
0x24, 0xa3, 0xb5, 0x71, 0x50, 0x28, 0xc1, 0xd7, 0xf8, 0x53, 0x11, 0xc6, 0x22, 0xe8, 0xdf, 0x84, 0x98, 0xff, 0x27, 0x19, 0xad, 0x8e, 0x83, 0x42, 0x09, 0xbe, 0xc6, 0xbf, 0x8a, 0x30, 0x16, 0x41,
0xf2, 0xbd, 0xbe, 0xe5, 0x52, 0x87, 0x3e, 0xa8, 0x9e, 0xe0, 0x42, 0x36, 0x32, 0xfd, 0xae, 0x48, 0xff, 0x06, 0x94, 0xef, 0xf5, 0x2d, 0x97, 0x3a, 0xf4, 0x41, 0xf5, 0x04, 0x17, 0xb2, 0x91, 0xe9,
0xfd, 0xba, 0xc4, 0x32, 0x97, 0x86, 0x83, 0xfa, 0x7c, 0xf0, 0x25, 0xa4, 0x08, 0x49, 0xea, 0xcf, 0x77, 0x45, 0xea, 0xd7, 0x25, 0x96, 0xb9, 0x34, 0x1c, 0xd4, 0xe7, 0x83, 0x91, 0x90, 0x22, 0x24,
0x40, 0x69, 0xd7, 0xf3, 0xc4, 0xf1, 0x28, 0x9b, 0xf3, 0x2c, 0x25, 0x99, 0x9e, 0xd7, 0x11, 0x60, 0xa9, 0x3f, 0x03, 0xa5, 0x5d, 0xcf, 0x13, 0xdb, 0xa3, 0x6c, 0xce, 0xb3, 0x94, 0x64, 0x7a, 0x5e,
0x7c, 0x4b, 0xaf, 0x41, 0xd1, 0x71, 0x69, 0x75, 0x66, 0x5d, 0xdb, 0x28, 0x9a, 0x27, 0x99, 0x53, 0x47, 0x80, 0xf1, 0x25, 0xbd, 0x06, 0x45, 0xc7, 0xa5, 0xd5, 0x99, 0x35, 0x6d, 0xbd, 0x68, 0x9e,
0xb7, 0x5d, 0x2a, 0x00, 0xd8, 0x86, 0xde, 0x86, 0xb2, 0xe3, 0xd2, 0x56, 0xc7, 0x69, 0xe3, 0x6a, 0x64, 0x4e, 0xdd, 0x72, 0xa9, 0x00, 0x60, 0x0b, 0x7a, 0x1b, 0xca, 0x8e, 0x4b, 0x5b, 0x1d, 0xa7,
0x99, 0x4b, 0xf8, 0x5c, 0x1e, 0x33, 0x6e, 0x4b, 0x5c, 0x21, 0x67, 0xf0, 0x25, 0xe5, 0x0c, 0x08, 0x8d, 0xab, 0x65, 0x2e, 0xe1, 0x73, 0x79, 0xcc, 0xb8, 0x25, 0x71, 0x85, 0x9c, 0xc1, 0x48, 0xca,
0xeb, 0x5f, 0x80, 0x13, 0x84, 0xfa, 0x8e, 0xbb, 0x57, 0x3d, 0xce, 0xdd, 0xba, 0x30, 0x1c, 0xd4, 0x19, 0x10, 0xd6, 0x3f, 0x07, 0x27, 0x08, 0xf5, 0x1d, 0x77, 0xaf, 0x7a, 0x9c, 0xbb, 0x75, 0x61,
0xe7, 0x5a, 0x7c, 0x45, 0x80, 0xca, 0x6d, 0xdd, 0x83, 0x39, 0xf1, 0x9f, 0x10, 0x68, 0x96, 0x0b, 0x38, 0xa8, 0xcf, 0xb5, 0xf8, 0x8c, 0x00, 0x95, 0xcb, 0xba, 0x07, 0x73, 0xe2, 0x9f, 0x10, 0x68,
0xf4, 0x42, 0x1e, 0x81, 0x5a, 0x11, 0xba, 0x48, 0xf1, 0xb1, 0x05, 0xc1, 0x2b, 0xce, 0xc1, 0xd8, 0x96, 0x0b, 0xf4, 0x42, 0x1e, 0x81, 0x5a, 0x11, 0xba, 0x48, 0xf1, 0xb1, 0x09, 0xc1, 0x2b, 0xce,
0x82, 0x65, 0x15, 0xff, 0x9a, 0xd3, 0xa1, 0xd8, 0xd7, 0xcf, 0x41, 0x99, 0xc8, 0x4a, 0x21, 0x43, 0x41, 0xff, 0x3c, 0xcc, 0x1c, 0x60, 0x9f, 0x6d, 0xb1, 0x2a, 0x70, 0xd1, 0x16, 0x87, 0x83, 0xfa,
0x31, 0x3c, 0x14, 0x41, 0x05, 0x41, 0x21, 0x84, 0xf1, 0x1b, 0x0d, 0x4e, 0x27, 0xed, 0x42, 0xa8, 0xc9, 0x1d, 0x31, 0x25, 0xe0, 0x03, 0x00, 0x63, 0x13, 0x96, 0x55, 0x5e, 0xd7, 0x9d, 0x0e, 0xc5,
0xe5, 0xb6, 0x27, 0x89, 0x67, 0x07, 0x20, 0x0c, 0x2b, 0x96, 0x1d, 0xd8, 0x81, 0x7d, 0x7e, 0xaa, 0xbe, 0x7e, 0x1e, 0xca, 0x44, 0x56, 0x15, 0x19, 0xb6, 0xe1, 0x06, 0x0a, 0xaa, 0x0d, 0x0a, 0x21,
0x50, 0x8e, 0xd2, 0x51, 0xb8, 0x44, 0x50, 0x8c, 0xb8, 0x71, 0x69, 0x54, 0x4c, 0xe9, 0xa1, 0x35, 0x8c, 0x5f, 0x6b, 0x70, 0x3a, 0x69, 0x43, 0x42, 0x2d, 0xb7, 0x3d, 0x49, 0xec, 0x3b, 0x00, 0x61,
0x28, 0x39, 0x2e, 0x15, 0xf5, 0xba, 0x68, 0x96, 0x99, 0x88, 0xdb, 0x2e, 0x25, 0x88, 0xaf, 0x1a, 0x08, 0xb2, 0x4c, 0xc2, 0x36, 0xf7, 0xf3, 0x53, 0x85, 0x7d, 0x94, 0xba, 0xc2, 0x29, 0x82, 0x62,
0x2f, 0xc3, 0x4a, 0xa2, 0xc0, 0x88, 0x74, 0x90, 0xd3, 0x4c, 0x0f, 0x47, 0xce, 0x7d, 0xf8, 0x8f, 0xc4, 0x8d, 0xcb, 0xa3, 0x62, 0x4a, 0x6f, 0xae, 0x42, 0xc9, 0x71, 0xa9, 0xa8, 0xed, 0x45, 0xb3,
0x8e, 0x61, 0xd6, 0x91, 0x36, 0x0b, 0xba, 0x86, 0x9c, 0x81, 0x28, 0x90, 0xa3, 0xe2, 0x1c, 0xac, 0xcc, 0x44, 0xdc, 0x72, 0x29, 0x41, 0x7c, 0xd6, 0x78, 0x19, 0x56, 0x12, 0xc5, 0x48, 0xa4, 0x8e,
0x10, 0x14, 0x51, 0x36, 0x4c, 0x38, 0x93, 0x19, 0x2f, 0xfa, 0xe7, 0x61, 0x46, 0xc4, 0x86, 0x90, 0x9c, 0x66, 0x7a, 0x38, 0x92, 0x23, 0xc2, 0x3f, 0x3a, 0x86, 0x59, 0x47, 0xda, 0x2c, 0xe8, 0x30,
0x60, 0xd6, 0x9c, 0x1b, 0x0e, 0xea, 0x33, 0x02, 0x82, 0xa0, 0x60, 0xcf, 0xf8, 0xa1, 0x06, 0x4b, 0x72, 0x06, 0xad, 0x40, 0x8e, 0x0a, 0x79, 0x30, 0x43, 0x50, 0x44, 0xd9, 0x30, 0xe1, 0x6c, 0x66,
0xac, 0x37, 0x08, 0x68, 0x88, 0xe2, 0x7e, 0x2f, 0xa3, 0xb8, 0xe7, 0x72, 0x65, 0xf8, 0xcf, 0x44, 0x6c, 0xe9, 0xff, 0x0f, 0x33, 0x22, 0x8e, 0x84, 0x04, 0xb3, 0xe6, 0xdc, 0x70, 0x50, 0x9f, 0x11,
0x55, 0xfd, 0xc3, 0x82, 0x2a, 0x88, 0xd0, 0xe2, 0x1d, 0x28, 0xb3, 0xf6, 0xd0, 0xb6, 0xa8, 0x25, 0x10, 0x04, 0x05, 0x6b, 0xc6, 0x0f, 0x34, 0x58, 0x62, 0x7d, 0x44, 0x40, 0x43, 0x34, 0x02, 0xf7,
0x45, 0x78, 0x76, 0x5c, 0xce, 0x21, 0x0d, 0x06, 0xcd, 0xda, 0xa3, 0xd7, 0x76, 0xbf, 0x83, 0xdb, 0x32, 0x1a, 0x81, 0x5c, 0xae, 0x0c, 0xff, 0x4c, 0xd4, 0x01, 0x7c, 0x58, 0x50, 0x05, 0x11, 0x5a,
0xf4, 0x55, 0x4c, 0xad, 0x28, 0x90, 0xa2, 0x35, 0x14, 0x52, 0x65, 0x5e, 0x77, 0x3d, 0x1b, 0xf3, 0xbc, 0x03, 0x65, 0xd6, 0x4a, 0xda, 0x16, 0xb5, 0xa4, 0x08, 0xcf, 0x8e, 0xcb, 0x4f, 0xa4, 0xc1,
0x3a, 0x58, 0x50, 0xbd, 0x7e, 0x43, 0xae, 0xa3, 0x10, 0x22, 0x51, 0x37, 0x8b, 0x13, 0xd5, 0xcd, 0xa0, 0x59, 0x2b, 0xf5, 0xda, 0xee, 0xb7, 0x71, 0x9b, 0xbe, 0x8a, 0xa9, 0x15, 0x05, 0x52, 0x34,
0xfb, 0xb0, 0xe4, 0x26, 0x2d, 0x5c, 0x2d, 0x71, 0x65, 0x9a, 0x87, 0xd8, 0x33, 0x89, 0x66, 0x9e, 0x87, 0x42, 0xaa, 0xcc, 0xeb, 0xae, 0x67, 0x63, 0x5e, 0x33, 0x0b, 0xaa, 0xd7, 0x6f, 0xca, 0x79,
0x91, 0xbc, 0x46, 0x7d, 0x86, 0x46, 0x99, 0x18, 0x7f, 0xd5, 0x60, 0x65, 0xc4, 0xa6, 0xaf, 0x38, 0x14, 0x42, 0x24, 0x6a, 0x6c, 0x71, 0xa2, 0x1a, 0x7b, 0x1f, 0x96, 0xdc, 0xa4, 0x85, 0xab, 0x25,
0x84, 0xea, 0xdf, 0x18, 0xb1, 0x6b, 0x63, 0x32, 0xbb, 0x32, 0x6c, 0x6e, 0xd5, 0xd0, 0x4a, 0xc1, 0xae, 0x4c, 0xf3, 0x10, 0x7b, 0x26, 0xd1, 0xcc, 0xb3, 0x92, 0xd7, 0xa8, 0xcf, 0xd0, 0x28, 0x13,
0x4a, 0xcc, 0xa6, 0x6f, 0xc0, 0x71, 0x87, 0xe2, 0x6e, 0x90, 0x00, 0x72, 0x68, 0x29, 0x72, 0xdd, 0xe3, 0xcf, 0x1a, 0xac, 0x8c, 0xd8, 0xf4, 0x15, 0x87, 0x50, 0xfd, 0xeb, 0x23, 0x76, 0x6d, 0x4c,
0xbc, 0xa4, 0x7d, 0x7c, 0x9b, 0x51, 0x41, 0x82, 0x98, 0xf1, 0xc7, 0x02, 0x2c, 0xef, 0x78, 0x76, 0x66, 0x57, 0x86, 0xcd, 0xad, 0x1a, 0x5a, 0x29, 0x98, 0x89, 0xd9, 0xf4, 0x0d, 0x38, 0xee, 0x50,
0xab, 0xbd, 0x8f, 0xed, 0x7e, 0xc7, 0x71, 0xf7, 0xae, 0x7a, 0x2e, 0xc5, 0xf7, 0xe9, 0x11, 0x04, 0xdc, 0x0d, 0x12, 0x40, 0x0e, 0x2d, 0x45, 0x5e, 0x9c, 0x97, 0xb4, 0x8f, 0x6f, 0x31, 0x2a, 0x48,
0xc9, 0x5b, 0x50, 0x22, 0x3d, 0xdc, 0x96, 0xb5, 0xf9, 0xd2, 0x78, 0x7d, 0xd2, 0x64, 0x6c, 0xf5, 0x10, 0x33, 0xfe, 0x50, 0x80, 0xe5, 0x6d, 0xcf, 0x6e, 0xb5, 0xf7, 0xb1, 0xdd, 0xef, 0x38, 0xee,
0x70, 0x3b, 0x4a, 0x98, 0xec, 0x0b, 0x71, 0x8a, 0xfa, 0x3b, 0xac, 0x9a, 0x58, 0xb4, 0x4f, 0x78, 0xde, 0x35, 0xcf, 0xa5, 0xf8, 0x3e, 0x3d, 0x82, 0x20, 0x79, 0x0b, 0x4a, 0xa4, 0x87, 0xdb, 0xb2,
0x30, 0xcd, 0x6d, 0x5e, 0x9e, 0x82, 0x36, 0xc7, 0x37, 0x2b, 0x92, 0xfa, 0x09, 0xf1, 0x8d, 0x24, 0x8e, 0x5f, 0x1e, 0xaf, 0x4f, 0x9a, 0x8c, 0xad, 0x1e, 0x6e, 0x47, 0x09, 0x93, 0x8d, 0x10, 0xa7,
0x5d, 0xe3, 0x43, 0x0d, 0xaa, 0x69, 0x68, 0x47, 0x10, 0x07, 0x6f, 0xaa, 0x71, 0xb0, 0x99, 0x5f, 0xa8, 0xbf, 0xc3, 0x2a, 0x8f, 0x45, 0xfb, 0x84, 0x07, 0xd3, 0xdc, 0xc6, 0x95, 0x29, 0x68, 0x73,
0xb7, 0x8c, 0x50, 0x78, 0x2f, 0x43, 0x27, 0x66, 0x58, 0xfd, 0x32, 0x9c, 0x14, 0x59, 0x1a, 0xdb, 0x7c, 0xb3, 0x22, 0xa9, 0x9f, 0x10, 0x63, 0x24, 0xe9, 0x1a, 0x1f, 0x6a, 0x50, 0x4d, 0x43, 0x3b,
0x2c, 0xb4, 0x64, 0x2e, 0x5f, 0x96, 0x84, 0x4e, 0xb6, 0x62, 0x7b, 0x48, 0x81, 0xd4, 0x5f, 0x84, 0x82, 0x38, 0x78, 0x53, 0x8d, 0x83, 0x8d, 0xfc, 0xba, 0x65, 0x84, 0xc2, 0x7b, 0x19, 0x3a, 0x31,
0x4a, 0xcf, 0xa3, 0xd8, 0xa5, 0x8e, 0xd5, 0x09, 0x06, 0x30, 0x96, 0x3a, 0x79, 0xfe, 0xda, 0x51, 0xc3, 0xea, 0x57, 0xe0, 0xa4, 0xc8, 0xd2, 0xd8, 0x66, 0xa1, 0x25, 0x73, 0xf9, 0xb2, 0x24, 0x74,
0x76, 0x50, 0x02, 0xd2, 0xf8, 0x95, 0x06, 0x67, 0xb3, 0xbd, 0xa3, 0x7f, 0x17, 0x2a, 0x81, 0xc6, 0xb2, 0x15, 0x5b, 0x43, 0x0a, 0xa4, 0xfe, 0x22, 0x54, 0x7a, 0x1e, 0xc5, 0x2e, 0x75, 0xac, 0x4e,
0x57, 0x3b, 0x96, 0xd3, 0x0d, 0xea, 0xc2, 0x97, 0x26, 0xeb, 0x66, 0x39, 0x4e, 0x44, 0x5b, 0xba, 0x70, 0x58, 0x63, 0xa9, 0x93, 0xe7, 0xaf, 0x6d, 0x65, 0x05, 0x25, 0x20, 0x8d, 0x5f, 0x6a, 0x70,
0xfc, 0xb4, 0xd4, 0xa9, 0xa2, 0x80, 0x11, 0x94, 0x60, 0x65, 0xfc, 0xba, 0x00, 0xf3, 0x0a, 0xc8, 0x2e, 0xdb, 0x3b, 0xfa, 0x77, 0xa0, 0x12, 0x68, 0x7c, 0xad, 0x63, 0x39, 0xdd, 0xa0, 0x2e, 0x7c,
0x11, 0x1c, 0x99, 0xd7, 0x95, 0x23, 0xd3, 0xcc, 0xa3, 0x66, 0xd6, 0x59, 0xb9, 0x9d, 0x38, 0x2b, 0x71, 0xb2, 0xce, 0x97, 0xe3, 0x44, 0xb4, 0xa5, 0xcb, 0x4f, 0x4b, 0x9d, 0x2a, 0x0a, 0x18, 0x41,
0x17, 0xf2, 0x10, 0x1d, 0x7f, 0x48, 0x86, 0x1a, 0xd4, 0x14, 0xf8, 0xab, 0x9e, 0x4b, 0xfa, 0x5d, 0x09, 0x56, 0xc6, 0xaf, 0x0a, 0x30, 0xaf, 0x80, 0x1c, 0xc1, 0x96, 0x79, 0x5d, 0xd9, 0x32, 0xcd,
0x36, 0x31, 0xdd, 0xc1, 0x3e, 0x66, 0xcd, 0xcf, 0x39, 0x28, 0x5b, 0x3d, 0xe7, 0xba, 0xef, 0xf5, 0x3c, 0x6a, 0x66, 0xed, 0x95, 0xdb, 0x89, 0xbd, 0x72, 0x31, 0x0f, 0xd1, 0xf1, 0x9b, 0x64, 0xa8,
0x7b, 0xc9, 0xf6, 0xe0, 0xca, 0xce, 0x36, 0x5f, 0x47, 0x21, 0x04, 0x83, 0x0e, 0x24, 0x92, 0x65, 0x41, 0x4d, 0x81, 0xbf, 0xe6, 0xb9, 0xa4, 0xdf, 0x65, 0xa7, 0xab, 0x3b, 0xd8, 0xc7, 0xac, 0xf9,
0x22, 0x36, 0x88, 0xc8, 0x09, 0x25, 0x84, 0x08, 0x1b, 0xab, 0x52, 0x66, 0x63, 0x65, 0x42, 0xb1, 0x39, 0x0f, 0x65, 0xab, 0xe7, 0xdc, 0xf0, 0xbd, 0x7e, 0x2f, 0xd9, 0x1e, 0x5c, 0xdd, 0xde, 0xe2,
0xef, 0xd8, 0xb2, 0xe5, 0x7c, 0x56, 0x02, 0x14, 0x6f, 0x6e, 0x6f, 0x7d, 0x3c, 0xa8, 0x3f, 0x93, 0xf3, 0x28, 0x84, 0x60, 0xd0, 0x81, 0x44, 0xb2, 0x4c, 0xc4, 0x0e, 0x2d, 0xf2, 0x34, 0x13, 0x42,
0x75, 0xef, 0x41, 0x1f, 0xf4, 0x30, 0x69, 0xdc, 0xdc, 0xde, 0x42, 0x0c, 0xd9, 0x78, 0x5f, 0x83, 0x84, 0x8d, 0x55, 0x29, 0xb3, 0xb1, 0x32, 0xa1, 0xd8, 0x77, 0x6c, 0xd9, 0x9e, 0x3e, 0x2b, 0x01,
0x25, 0x45, 0xc9, 0x23, 0x48, 0x01, 0x3b, 0x6a, 0x0a, 0xf8, 0x62, 0x0e, 0x97, 0x65, 0x9c, 0xfd, 0x8a, 0xb7, 0xb6, 0x36, 0x3f, 0x1e, 0xd4, 0x9f, 0xc9, 0xba, 0x23, 0xa1, 0x0f, 0x7a, 0x98, 0x34,
0x9f, 0x15, 0x61, 0x55, 0x81, 0x8b, 0x4d, 0x8b, 0x9f, 0x7c, 0x58, 0xbf, 0x0b, 0xf3, 0xe1, 0xf5, 0x6e, 0x6d, 0x6d, 0x22, 0x86, 0x6c, 0xbc, 0xaf, 0xc1, 0x92, 0xa2, 0xe4, 0x11, 0xa4, 0x80, 0x6d,
0xd1, 0x35, 0xdf, 0xeb, 0xca, 0xf8, 0xfe, 0x4a, 0x0e, 0xbd, 0x62, 0xf3, 0x6e, 0x10, 0x5c, 0x62, 0x35, 0x05, 0x7c, 0x21, 0x87, 0xcb, 0x32, 0xf6, 0xfe, 0x4f, 0x8b, 0x70, 0x46, 0x81, 0x8b, 0x9d,
0xe2, 0xb8, 0x1e, 0x27, 0x8c, 0x54, 0x3e, 0xb9, 0xaf, 0x6e, 0xf4, 0x0e, 0x54, 0x6c, 0x65, 0xe8, 0x2c, 0x3f, 0xf9, 0xb0, 0x7e, 0x17, 0xe6, 0xc3, 0xab, 0xa6, 0xeb, 0xbe, 0xd7, 0x95, 0xf1, 0xfd,
0xaf, 0x96, 0x26, 0xb9, 0xbf, 0x52, 0x2f, 0x0a, 0xa2, 0x14, 0xa3, 0xae, 0xa3, 0x04, 0x6d, 0xe3, 0xe5, 0x1c, 0x7a, 0xc5, 0xce, 0xc6, 0x41, 0x70, 0x89, 0xd3, 0xc9, 0x8d, 0x38, 0x61, 0xa4, 0xf2,
0x1f, 0x1a, 0x3c, 0x95, 0xa1, 0xe5, 0x11, 0x44, 0xd9, 0xdb, 0x6a, 0x94, 0x3d, 0x3f, 0x95, 0x37, 0xc9, 0x7d, 0xcd, 0xa3, 0x77, 0xa0, 0x62, 0x2b, 0x17, 0x04, 0xd5, 0xd2, 0x24, 0x77, 0x5d, 0xea,
0x32, 0xe2, 0xed, 0xe7, 0x1a, 0xac, 0x1f, 0xe6, 0xbf, 0x9c, 0xc9, 0x61, 0x1d, 0x4a, 0x77, 0x1d, 0xa5, 0x42, 0x94, 0x62, 0xd4, 0x79, 0x94, 0xa0, 0x6d, 0xfc, 0x4d, 0x83, 0xa7, 0x32, 0xb4, 0x3c,
0xd7, 0x96, 0xfd, 0x66, 0x78, 0xdc, 0xbf, 0xe6, 0xb8, 0x36, 0xe2, 0x3b, 0x61, 0x42, 0x28, 0x66, 0x82, 0x28, 0x7b, 0x5b, 0x8d, 0xb2, 0xe7, 0xa7, 0xf2, 0x46, 0x46, 0xbc, 0xfd, 0x4c, 0x83, 0xb5,
0xde, 0x3b, 0x3c, 0xd4, 0xe0, 0xe9, 0xb1, 0xd5, 0x61, 0x82, 0x69, 0xed, 0xcb, 0xb0, 0xd0, 0x77, 0xc3, 0xfc, 0x97, 0x33, 0x39, 0xac, 0x41, 0xe9, 0xae, 0xe3, 0xda, 0xb2, 0xdf, 0x0c, 0xb7, 0xfb,
0x49, 0xdf, 0xa1, 0x2c, 0x60, 0xe2, 0x05, 0xef, 0xd4, 0x70, 0x50, 0x5f, 0xb8, 0xa9, 0x6e, 0xa1, 0x57, 0x1d, 0xd7, 0x46, 0x7c, 0x25, 0x4c, 0x08, 0xc5, 0xcc, 0x3b, 0x8a, 0x87, 0x1a, 0x3c, 0x3d,
0x24, 0xac, 0xf1, 0xdb, 0x42, 0x22, 0x9f, 0xf0, 0xf2, 0x7b, 0x1d, 0x96, 0x62, 0xe5, 0x87, 0x90, 0xb6, 0x3a, 0x4c, 0x70, 0x5a, 0xfb, 0x12, 0x2c, 0xf4, 0x5d, 0xd2, 0x77, 0x28, 0x0b, 0x98, 0x78,
0xd8, 0x0d, 0x53, 0xd8, 0xbd, 0xa2, 0x24, 0x00, 0x1a, 0xc5, 0x61, 0x47, 0xad, 0x17, 0x37, 0xf5, 0xc1, 0x3b, 0x35, 0x1c, 0xd4, 0x17, 0x6e, 0xa9, 0x4b, 0x28, 0x09, 0x6b, 0xfc, 0xa6, 0x90, 0xc8,
0xff, 0xf3, 0xa8, 0x29, 0x1b, 0x48, 0xe5, 0xa3, 0xef, 0x40, 0x25, 0xba, 0x48, 0x63, 0x9d, 0xb4, 0x27, 0xbc, 0xfc, 0xde, 0x80, 0xa5, 0x58, 0xf9, 0x21, 0x24, 0x76, 0x1b, 0x15, 0x76, 0xaf, 0x28,
0x74, 0xc3, 0x46, 0x70, 0x16, 0xae, 0x28, 0xbb, 0x1f, 0x8f, 0xac, 0xa0, 0x04, 0xbe, 0xf1, 0x9f, 0x09, 0x80, 0x46, 0x71, 0xd8, 0x56, 0xeb, 0xc5, 0x4d, 0xfd, 0xbf, 0xdc, 0x6a, 0xca, 0x02, 0x52,
0x02, 0x9c, 0x4a, 0x29, 0x47, 0x53, 0x5d, 0xc3, 0x7d, 0x0b, 0x20, 0xa2, 0x2e, 0x6d, 0xd2, 0xc8, 0xf9, 0xe8, 0xdb, 0x50, 0x89, 0x2e, 0xdd, 0x58, 0x27, 0x2d, 0xdd, 0xb0, 0x1e, 0xec, 0x85, 0xab,
0x77, 0x99, 0x68, 0x56, 0xf8, 0x5c, 0x1d, 0xad, 0xc6, 0x28, 0xea, 0x04, 0xe6, 0x7c, 0x4c, 0xb0, 0xca, 0xea, 0xc7, 0x23, 0x33, 0x28, 0x81, 0x6f, 0xfc, 0xbb, 0x00, 0xa7, 0x52, 0xca, 0xd1, 0x54,
0x7f, 0x80, 0xed, 0x6b, 0x9e, 0x2f, 0x2f, 0xdd, 0x5e, 0xca, 0x61, 0xf4, 0x91, 0xd2, 0x69, 0x9e, 0x57, 0x76, 0xdf, 0x04, 0x88, 0xa8, 0x4b, 0x9b, 0x34, 0xf2, 0x5d, 0x3c, 0x9a, 0x15, 0x7e, 0xae,
0x92, 0x2a, 0xcd, 0xa1, 0x88, 0x30, 0x8a, 0x73, 0xd1, 0x5b, 0xb0, 0x62, 0xe3, 0xf8, 0xed, 0x25, 0x8e, 0x66, 0x63, 0x14, 0x75, 0x02, 0x73, 0x3e, 0x26, 0xd8, 0x3f, 0xc0, 0xf6, 0x75, 0xcf, 0x97,
0x4f, 0x2b, 0xd8, 0xe6, 0x15, 0xb1, 0x1c, 0xdd, 0x7b, 0x6e, 0xa5, 0x01, 0xa1, 0x74, 0x5c, 0xe3, 0x17, 0x74, 0x2f, 0xe5, 0x30, 0xfa, 0x48, 0xe9, 0x34, 0x4f, 0x49, 0x95, 0xe6, 0x50, 0x44, 0x18,
0xef, 0x1a, 0xac, 0x28, 0x92, 0xbd, 0x81, 0xbb, 0xbd, 0x8e, 0x45, 0x8f, 0x62, 0xac, 0xbc, 0xad, 0xc5, 0xb9, 0xe8, 0x2d, 0x58, 0xb1, 0x71, 0xfc, 0xa6, 0x93, 0xa7, 0x15, 0x6c, 0xf3, 0x8a, 0x58,
0xb4, 0x3f, 0x2f, 0xe4, 0x30, 0x5f, 0x20, 0x64, 0x56, 0x1b, 0x64, 0xfc, 0x4d, 0x83, 0x33, 0xa9, 0x8e, 0xee, 0x48, 0x37, 0xd3, 0x80, 0x50, 0x3a, 0xae, 0xf1, 0x57, 0x0d, 0x56, 0x14, 0xc9, 0xde,
0x18, 0x47, 0x90, 0x68, 0xdf, 0x52, 0x13, 0xed, 0xc5, 0x29, 0xf4, 0xca, 0x48, 0xb3, 0x8f, 0xb3, 0xc0, 0xdd, 0x5e, 0xc7, 0xa2, 0x47, 0x71, 0xac, 0xbc, 0xad, 0xb4, 0x3f, 0x2f, 0xe4, 0x30, 0x5f,
0xb4, 0x6a, 0x89, 0x31, 0xe9, 0xb3, 0xd7, 0xaf, 0x1a, 0x1f, 0x14, 0x95, 0xb6, 0x9b, 0x1c, 0x45, 0x20, 0x64, 0x56, 0x1b, 0x64, 0xfc, 0x45, 0x83, 0xb3, 0xa9, 0x18, 0x47, 0x90, 0x68, 0xdf, 0x52,
0x7f, 0xa2, 0x66, 0x94, 0xc2, 0x44, 0x19, 0x65, 0x24, 0xd1, 0x16, 0x73, 0x26, 0x5a, 0x42, 0xa6, 0x13, 0xed, 0xa5, 0x29, 0xf4, 0xca, 0x48, 0xb3, 0x8f, 0xb3, 0xb4, 0x6a, 0x89, 0x63, 0xd2, 0x67,
0x4b, 0xb4, 0xb7, 0x61, 0x5e, 0xad, 0x3e, 0xa5, 0x09, 0xdf, 0xbb, 0x38, 0xe9, 0x96, 0x52, 0x9d, 0xaf, 0x5f, 0x35, 0x3e, 0x28, 0x2a, 0x6d, 0x37, 0x39, 0x8a, 0xfe, 0x44, 0xcd, 0x28, 0x85, 0x89,
0x54, 0x4a, 0xfa, 0x2b, 0xb0, 0x4c, 0xa8, 0xdf, 0x6f, 0xd3, 0xbe, 0x8f, 0xed, 0xd8, 0x83, 0xc5, 0x32, 0xca, 0x48, 0xa2, 0x2d, 0xe6, 0x4c, 0xb4, 0x84, 0x4c, 0x97, 0x68, 0x6f, 0xc3, 0xbc, 0x5a,
0x71, 0x9e, 0x4f, 0xaa, 0xc3, 0x41, 0x7d, 0xb9, 0x95, 0xb2, 0x8f, 0x52, 0xb1, 0x92, 0x9d, 0x33, 0x7d, 0x4a, 0x13, 0xbe, 0x8d, 0x71, 0xd2, 0x2d, 0xa5, 0x3a, 0xa9, 0x94, 0xf4, 0x57, 0x60, 0x99,
0x21, 0x9f, 0xe6, 0xce, 0x99, 0x64, 0x75, 0x32, 0xef, 0xab, 0x9d, 0x73, 0xdc, 0x6b, 0x9f, 0x85, 0x50, 0xbf, 0xdf, 0xa6, 0x7d, 0x1f, 0xdb, 0xb1, 0xc7, 0x8d, 0xe3, 0x3c, 0x9f, 0x54, 0x87, 0x83,
0xce, 0x79, 0x4c, 0x94, 0x8d, 0xed, 0x9c, 0x69, 0xca, 0xbb, 0x95, 0xa8, 0x6a, 0x87, 0x94, 0xcd, 0xfa, 0x72, 0x2b, 0x65, 0x1d, 0xa5, 0x62, 0x25, 0x3b, 0x67, 0x42, 0x3e, 0xcd, 0x9d, 0x33, 0xc9,
0xe4, 0xf3, 0x54, 0xae, 0x87, 0xab, 0x37, 0x61, 0xe6, 0x0e, 0xbf, 0x7e, 0x9f, 0xb0, 0xef, 0x0e, 0xea, 0x64, 0xde, 0x57, 0x3b, 0xe7, 0xb8, 0xd7, 0x3e, 0x0b, 0x9d, 0xf3, 0x98, 0x28, 0x1b, 0xdb,
0x14, 0x15, 0x77, 0xf6, 0xe6, 0x82, 0x64, 0x35, 0x23, 0xbe, 0x09, 0x0a, 0xa8, 0x25, 0x3b, 0xed, 0x39, 0xd3, 0x94, 0x37, 0x2e, 0x51, 0xd5, 0x0e, 0x29, 0x9b, 0xc9, 0xa7, 0xac, 0x5c, 0x8f, 0x5c,
0xb8, 0x55, 0x3e, 0xcd, 0x9d, 0x76, 0x5c, 0xce, 0x8c, 0xf8, 0xfc, 0xb3, 0xda, 0x69, 0xa7, 0xfa, 0x6f, 0xc2, 0xcc, 0x1d, 0x7e, 0xfd, 0x3e, 0x61, 0xdf, 0x1d, 0x28, 0x2a, 0xee, 0xec, 0xcd, 0x05,
0xfb, 0xe8, 0x3b, 0x6d, 0x36, 0x79, 0xb1, 0xbf, 0xa4, 0x67, 0xb5, 0x83, 0x09, 0x3d, 0x9c, 0xbc, 0xc9, 0x6a, 0x46, 0x8c, 0x09, 0x0a, 0xa8, 0x25, 0x3b, 0xed, 0xb8, 0x55, 0x3e, 0xcd, 0x9d, 0x76,
0x6e, 0x04, 0x1b, 0x28, 0x82, 0x31, 0x3e, 0xd0, 0xa0, 0xa2, 0xba, 0x73, 0xaa, 0x46, 0xef, 0xa1, 0x5c, 0xce, 0x8c, 0xf8, 0xfc, 0xa3, 0xda, 0x69, 0xa7, 0xfa, 0xfb, 0xe8, 0x3b, 0x6d, 0x76, 0xf2,
0x06, 0xa7, 0x7c, 0x85, 0x4c, 0xfc, 0xfd, 0xf8, 0x42, 0x9e, 0x70, 0x12, 0x97, 0xc7, 0x4f, 0x49, 0x62, 0xbf, 0xa4, 0x67, 0xb5, 0x83, 0x13, 0x7a, 0x78, 0xf2, 0xba, 0x19, 0x2c, 0xa0, 0x08, 0xc6,
0x86, 0xa7, 0x52, 0x36, 0x51, 0x1a, 0x2b, 0xe3, 0x07, 0x1a, 0xa4, 0x01, 0xeb, 0x6e, 0xc6, 0xfb, 0xf8, 0x40, 0x83, 0x8a, 0xea, 0xce, 0xa9, 0x1a, 0xbd, 0x87, 0x1a, 0x9c, 0xf2, 0x15, 0x32, 0xf1,
0xc0, 0x66, 0x9e, 0xf7, 0x01, 0x19, 0xe9, 0x93, 0x3c, 0x0e, 0xfc, 0x33, 0x66, 0x51, 0xf1, 0x7b, 0xb7, 0xe6, 0x8b, 0x79, 0xc2, 0x49, 0x5c, 0x1e, 0x3f, 0x25, 0x19, 0x9e, 0x4a, 0x59, 0x44, 0x69,
0x89, 0xa9, 0x2c, 0xba, 0x0e, 0x25, 0x7e, 0x2c, 0x12, 0xd1, 0xb0, 0x65, 0x51, 0x0b, 0xf1, 0x1d, 0xac, 0x8c, 0xef, 0x6b, 0x90, 0x06, 0xac, 0xbb, 0x19, 0xef, 0x03, 0x1b, 0x79, 0xde, 0x07, 0x64,
0xdd, 0x87, 0x4a, 0x54, 0x00, 0xd8, 0x3a, 0x2f, 0x18, 0x87, 0x5e, 0xf9, 0x46, 0xa5, 0x24, 0xf1, 0xa4, 0x4f, 0xf2, 0x38, 0xf0, 0xf7, 0x98, 0x45, 0xc5, 0xb7, 0x15, 0x53, 0x59, 0x74, 0x0d, 0x4a,
0xf3, 0x0f, 0xae, 0x5c, 0x4b, 0xa1, 0x88, 0x12, 0x1c, 0x8c, 0x9f, 0x14, 0x60, 0x21, 0xf1, 0x6a, 0x7c, 0x5b, 0x24, 0xa2, 0x61, 0xd3, 0xa2, 0x16, 0xe2, 0x2b, 0xba, 0x0f, 0x95, 0xa8, 0x00, 0xb0,
0x9d, 0xfa, 0xd6, 0xae, 0x7d, 0xd2, 0x6f, 0xed, 0xdf, 0xd7, 0x60, 0xd9, 0x57, 0x05, 0x89, 0x47, 0x79, 0x5e, 0x30, 0x0e, 0xbd, 0xf2, 0x8d, 0x4a, 0x49, 0xe2, 0x53, 0x11, 0xae, 0x5c, 0x4b, 0xa1,
0xdc, 0x66, 0xae, 0x87, 0x77, 0x11, 0x72, 0x6b, 0x92, 0xfd, 0x72, 0xda, 0x2e, 0x4a, 0xe5, 0x66, 0x88, 0x12, 0x1c, 0x8c, 0x1f, 0x17, 0x60, 0x21, 0xf1, 0xc2, 0x9d, 0xfa, 0x2e, 0xaf, 0x7d, 0xd2,
0xfc, 0x48, 0x83, 0x54, 0x70, 0xdd, 0xcb, 0x88, 0xba, 0x8b, 0xf9, 0x5e, 0xa5, 0xc4, 0xef, 0x02, 0xef, 0xf2, 0xdf, 0xd3, 0x60, 0xd9, 0x57, 0x05, 0x89, 0x47, 0xdc, 0x46, 0xae, 0x47, 0x7a, 0x11,
0x26, 0x09, 0xbb, 0x3f, 0x14, 0xa1, 0x9a, 0xe5, 0x5a, 0xfd, 0x7b, 0x1a, 0xac, 0x08, 0x13, 0x26, 0x72, 0xab, 0x92, 0xfd, 0x72, 0xda, 0x2a, 0x4a, 0xe5, 0x66, 0xfc, 0x50, 0x83, 0x54, 0x70, 0xdd,
0x72, 0xd5, 0x74, 0x8e, 0x0a, 0x47, 0x9c, 0x5b, 0x69, 0x34, 0x51, 0x3a, 0x2b, 0x55, 0x88, 0xf8, 0xcb, 0x88, 0xba, 0x4b, 0xf9, 0x5e, 0xa5, 0xc4, 0x37, 0x04, 0x93, 0x84, 0xdd, 0xef, 0x8b, 0x50,
0xbc, 0x3b, 0xdd, 0x2f, 0x33, 0x46, 0x85, 0x50, 0x66, 0xe8, 0x74, 0x56, 0xca, 0x13, 0x5a, 0xe9, 0xcd, 0x72, 0xad, 0xfe, 0x5d, 0x0d, 0x56, 0x84, 0x09, 0x13, 0xb9, 0x6a, 0x3a, 0x47, 0x85, 0x47,
0xd0, 0x27, 0xb4, 0x6f, 0xc3, 0x8c, 0xcf, 0xa7, 0x50, 0xd6, 0x8c, 0x4d, 0xf0, 0x34, 0x9a, 0xfe, 0x9c, 0x9d, 0x34, 0x9a, 0x28, 0x9d, 0x95, 0x2a, 0x44, 0xfc, 0xbc, 0x3b, 0xdd, 0x57, 0x1c, 0xa3,
0x53, 0x9f, 0xa8, 0x40, 0x8a, 0x6f, 0x82, 0x02, 0xaa, 0xc6, 0xef, 0x34, 0x18, 0x89, 0xf6, 0xa9, 0x42, 0x28, 0x67, 0xe8, 0x74, 0x56, 0xca, 0x13, 0x5a, 0xe9, 0xd0, 0x27, 0xb4, 0x6f, 0xc1, 0x8c,
0xd2, 0x85, 0x05, 0xd0, 0xfb, 0x1f, 0x0d, 0x1a, 0xb2, 0x88, 0x59, 0x31, 0x46, 0xd4, 0x34, 0x1f, 0xcf, 0x4f, 0xa1, 0xac, 0x19, 0x9b, 0xe0, 0x69, 0x34, 0xfd, 0xb3, 0xa0, 0xa8, 0x40, 0x8a, 0x31,
0x3d, 0xa9, 0x1d, 0x7b, 0xfc, 0xa4, 0x76, 0xec, 0xa3, 0x27, 0xb5, 0x63, 0x0f, 0x87, 0x35, 0xed, 0x41, 0x01, 0x55, 0xe3, 0xb7, 0x1a, 0x8c, 0x44, 0xfb, 0x54, 0xe9, 0xc2, 0x02, 0xe8, 0xfd, 0x97,
0xd1, 0xb0, 0xa6, 0x3d, 0x1e, 0xd6, 0xb4, 0x8f, 0x86, 0x35, 0xed, 0x5f, 0xc3, 0x9a, 0xf6, 0xde, 0x06, 0x0d, 0x59, 0xc4, 0xac, 0x18, 0x23, 0x6a, 0x9a, 0x8f, 0x9e, 0xd4, 0x8e, 0x3d, 0x7e, 0x52,
0xbf, 0x6b, 0xc7, 0xde, 0x5e, 0x1b, 0xf7, 0xa3, 0xc0, 0xff, 0x06, 0x00, 0x00, 0xff, 0xff, 0x15, 0x3b, 0xf6, 0xd1, 0x93, 0xda, 0xb1, 0x87, 0xc3, 0x9a, 0xf6, 0x68, 0x58, 0xd3, 0x1e, 0x0f, 0x6b,
0x38, 0x05, 0x05, 0x33, 0x28, 0x00, 0x00, 0xda, 0x47, 0xc3, 0x9a, 0xf6, 0x8f, 0x61, 0x4d, 0x7b, 0xef, 0x9f, 0xb5, 0x63, 0x6f, 0xaf, 0x8e,
0xfb, 0x80, 0xf0, 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x03, 0x20, 0x09, 0x51, 0x5f, 0x28, 0x00,
0x00,
} }
func (m *AllocationResult) Marshal() (dAtA []byte, err error) { func (m *AllocationResult) Marshal() (dAtA []byte, err error) {
@ -1747,6 +1749,13 @@ func (m *NamedResourcesAttributeValue) MarshalToSizedBuffer(dAtA []byte) (int, e
_ = i _ = i
var l int var l int
_ = l _ = l
if m.VersionValue != nil {
i -= len(*m.VersionValue)
copy(dAtA[i:], *m.VersionValue)
i = encodeVarintGenerated(dAtA, i, uint64(len(*m.VersionValue)))
i--
dAtA[i] = 0x52
}
if m.StringSliceValue != nil { if m.StringSliceValue != nil {
{ {
size, err := m.StringSliceValue.MarshalToSizedBuffer(dAtA[:i]) size, err := m.StringSliceValue.MarshalToSizedBuffer(dAtA[:i])
@ -3592,6 +3601,10 @@ func (m *NamedResourcesAttributeValue) Size() (n int) {
l = m.StringSliceValue.Size() l = m.StringSliceValue.Size()
n += 1 + l + sovGenerated(uint64(l)) n += 1 + l + sovGenerated(uint64(l))
} }
if m.VersionValue != nil {
l = len(*m.VersionValue)
n += 1 + l + sovGenerated(uint64(l))
}
return n return n
} }
@ -4289,6 +4302,7 @@ func (this *NamedResourcesAttributeValue) String() string {
`IntValue:` + valueToStringGenerated(this.IntValue) + `,`, `IntValue:` + valueToStringGenerated(this.IntValue) + `,`,
`IntSliceValue:` + strings.Replace(this.IntSliceValue.String(), "NamedResourcesIntSlice", "NamedResourcesIntSlice", 1) + `,`, `IntSliceValue:` + strings.Replace(this.IntSliceValue.String(), "NamedResourcesIntSlice", "NamedResourcesIntSlice", 1) + `,`,
`StringSliceValue:` + strings.Replace(this.StringSliceValue.String(), "NamedResourcesStringSlice", "NamedResourcesStringSlice", 1) + `,`, `StringSliceValue:` + strings.Replace(this.StringSliceValue.String(), "NamedResourcesStringSlice", "NamedResourcesStringSlice", 1) + `,`,
`VersionValue:` + valueToStringGenerated(this.VersionValue) + `,`,
`}`, `}`,
}, "") }, "")
return s return s
@ -5695,6 +5709,39 @@ func (m *NamedResourcesAttributeValue) Unmarshal(dAtA []byte) error {
return err return err
} }
iNdEx = postIndex iNdEx = postIndex
case 10:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field VersionValue", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthGenerated
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthGenerated
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
s := string(dAtA[iNdEx:postIndex])
m.VersionValue = &s
iNdEx = postIndex
default: default:
iNdEx = preIndex iNdEx = preIndex
skippy, err := skipGenerated(dAtA[iNdEx:]) skippy, err := skipGenerated(dAtA[iNdEx:])

View File

@ -134,6 +134,9 @@ message NamedResourcesAttributeValue {
// StringSliceValue is an array of strings. // StringSliceValue is an array of strings.
optional NamedResourcesStringSlice stringSlice = 9; optional NamedResourcesStringSlice stringSlice = 9;
// VersionValue is a semantic version according to semver.org spec 2.0.0.
optional string version = 10;
} }
// NamedResourcesFilter is used in ResourceFilterModel. // NamedResourcesFilter is used in ResourceFilterModel.

View File

@ -70,7 +70,8 @@ type NamedResourcesAttributeValue struct {
StringValue *string `json:"string,omitempty" protobuf:"bytes,5,opt,name=string"` StringValue *string `json:"string,omitempty" protobuf:"bytes,5,opt,name=string"`
// StringSliceValue is an array of strings. // StringSliceValue is an array of strings.
StringSliceValue *NamedResourcesStringSlice `json:"stringSlice,omitempty" protobuf:"bytes,9,rep,name=stringSlice"` StringSliceValue *NamedResourcesStringSlice `json:"stringSlice,omitempty" protobuf:"bytes,9,rep,name=stringSlice"`
// TODO: VersionValue *SemVersion `json:"version,omitempty" protobuf:"bytes,7,opt,name=version"` // VersionValue is a semantic version according to semver.org spec 2.0.0.
VersionValue *string `json:"version,omitempty" protobuf:"bytes,10,opt,name=version"`
} }
// NamedResourcesIntSlice contains a slice of 64-bit integers. // NamedResourcesIntSlice contains a slice of 64-bit integers.
@ -89,17 +90,6 @@ type NamedResourcesStringSlice struct {
Strings []string `json:"strings" protobuf:"bytes,1,opt,name=strings"` Strings []string `json:"strings" protobuf:"bytes,1,opt,name=strings"`
} }
// TODO
//
// A wrapper around https://pkg.go.dev/github.com/blang/semver/v4#Version which
// is encoded as a string. During decoding, it validates that the string
// can be parsed using tolerant parsing (currently trims spaces, removes a "v" prefix,
// adds a 0 patch number to versions with only major and minor components specified,
// and removes leading 0s).
// type NamedResourcesSemVersion struct {
// semver.Version
//}
// NamedResourcesRequest is used in ResourceRequestModel. // NamedResourcesRequest is used in ResourceRequestModel.
type NamedResourcesRequest struct { type NamedResourcesRequest struct {
// Selector is a CEL expression which must evaluate to true if a // Selector is a CEL expression which must evaluate to true if a

View File

@ -183,6 +183,11 @@ func (in *NamedResourcesAttributeValue) DeepCopyInto(out *NamedResourcesAttribut
*out = new(NamedResourcesStringSlice) *out = new(NamedResourcesStringSlice)
(*in).DeepCopyInto(*out) (*in).DeepCopyInto(*out)
} }
if in.VersionValue != nil {
in, out := &in.VersionValue, &out.VersionValue
*out = new(string)
**out = **in
}
return return
} }

View File

@ -65,7 +65,8 @@
"strings": [ "strings": [
"stringsValue" "stringsValue"
] ]
} },
"version": "versionValue"
} }
] ]
} }

View File

@ -47,5 +47,6 @@ namedResources:
stringSlice: stringSlice:
strings: strings:
- stringsValue - stringsValue
version: versionValue
name: nameValue name: nameValue
nodeName: nodeNameValue nodeName: nodeNameValue

View File

@ -12001,6 +12001,9 @@ var schemaYAML = typed.YAMLObject(`types:
- name: stringSlice - name: stringSlice
type: type:
namedType: io.k8s.api.resource.v1alpha2.NamedResourcesStringSlice namedType: io.k8s.api.resource.v1alpha2.NamedResourcesStringSlice
- name: version
type:
scalar: string
- name: io.k8s.api.resource.v1alpha2.NamedResourcesFilter - name: io.k8s.api.resource.v1alpha2.NamedResourcesFilter
map: map:
fields: fields:

View File

@ -90,3 +90,11 @@ func (b *NamedResourcesAttributeApplyConfiguration) WithStringSliceValue(value *
b.StringSliceValue = value b.StringSliceValue = value
return b return b
} }
// WithVersionValue sets the VersionValue field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the VersionValue field is set to the value of the last call.
func (b *NamedResourcesAttributeApplyConfiguration) WithVersionValue(value string) *NamedResourcesAttributeApplyConfiguration {
b.VersionValue = &value
return b
}

View File

@ -31,6 +31,7 @@ type NamedResourcesAttributeValueApplyConfiguration struct {
IntSliceValue *NamedResourcesIntSliceApplyConfiguration `json:"intSlice,omitempty"` IntSliceValue *NamedResourcesIntSliceApplyConfiguration `json:"intSlice,omitempty"`
StringValue *string `json:"string,omitempty"` StringValue *string `json:"string,omitempty"`
StringSliceValue *NamedResourcesStringSliceApplyConfiguration `json:"stringSlice,omitempty"` StringSliceValue *NamedResourcesStringSliceApplyConfiguration `json:"stringSlice,omitempty"`
VersionValue *string `json:"version,omitempty"`
} }
// NamedResourcesAttributeValueApplyConfiguration constructs an declarative configuration of the NamedResourcesAttributeValue type for use with // NamedResourcesAttributeValueApplyConfiguration constructs an declarative configuration of the NamedResourcesAttributeValue type for use with
@ -86,3 +87,11 @@ func (b *NamedResourcesAttributeValueApplyConfiguration) WithStringSliceValue(va
b.StringSliceValue = value b.StringSliceValue = value
return b return b
} }
// WithVersionValue sets the VersionValue field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the VersionValue field is set to the value of the last call.
func (b *NamedResourcesAttributeValueApplyConfiguration) WithVersionValue(value string) *NamedResourcesAttributeValueApplyConfiguration {
b.VersionValue = &value
return b
}

View File

@ -5,6 +5,7 @@ module k8s.io/dynamic-resource-allocation
go 1.22.0 go 1.22.0
require ( require (
github.com/blang/semver/v4 v4.0.0
github.com/go-logr/logr v1.4.1 github.com/go-logr/logr v1.4.1
github.com/google/cel-go v0.17.8 github.com/google/cel-go v0.17.8
github.com/google/go-cmp v0.6.0 github.com/google/go-cmp v0.6.0

View File

@ -6,6 +6,7 @@ github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/g
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=

View File

@ -21,6 +21,7 @@ import (
"fmt" "fmt"
"reflect" "reflect"
"github.com/blang/semver/v4"
"github.com/google/cel-go/cel" "github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/traits" "github.com/google/cel-go/common/types/traits"
@ -105,43 +106,54 @@ var valueTypes = map[string]struct {
celType *cel.Type celType *cel.Type
// get returns nil if the attribute doesn't have the type, otherwise // get returns nil if the attribute doesn't have the type, otherwise
// the value of that type. // the value of that type.
get func(attr resourceapi.NamedResourcesAttribute) any get func(attr resourceapi.NamedResourcesAttribute) (any, error)
}{ }{
"quantity": {apiservercel.QuantityType, func(attr resourceapi.NamedResourcesAttribute) any { "quantity": {apiservercel.QuantityType, func(attr resourceapi.NamedResourcesAttribute) (any, error) {
if attr.QuantityValue == nil { if attr.QuantityValue == nil {
return nil return nil, nil
} }
return apiservercel.Quantity{Quantity: attr.QuantityValue} return apiservercel.Quantity{Quantity: attr.QuantityValue}, nil
}}, }},
"bool": {cel.BoolType, func(attr resourceapi.NamedResourcesAttribute) any { "bool": {cel.BoolType, func(attr resourceapi.NamedResourcesAttribute) (any, error) {
if attr.BoolValue == nil { if attr.BoolValue == nil {
return nil return nil, nil
} }
return *attr.BoolValue return *attr.BoolValue, nil
}}, }},
"int": {cel.IntType, func(attr resourceapi.NamedResourcesAttribute) any { "int": {cel.IntType, func(attr resourceapi.NamedResourcesAttribute) (any, error) {
if attr.IntValue == nil { if attr.IntValue == nil {
return nil return nil, nil
} }
return *attr.IntValue return *attr.IntValue, nil
}}, }},
"intslice": {types.NewListType(cel.IntType), func(attr resourceapi.NamedResourcesAttribute) any { "intslice": {types.NewListType(cel.IntType), func(attr resourceapi.NamedResourcesAttribute) (any, error) {
if attr.IntSliceValue == nil { if attr.IntSliceValue == nil {
return nil return nil, nil
} }
return attr.IntSliceValue.Ints return attr.IntSliceValue.Ints, nil
}}, }},
"string": {cel.StringType, func(attr resourceapi.NamedResourcesAttribute) any { "string": {cel.StringType, func(attr resourceapi.NamedResourcesAttribute) (any, error) {
if attr.StringValue == nil { if attr.StringValue == nil {
return nil return nil, nil
} }
return *attr.StringValue return *attr.StringValue, nil
}}, }},
"stringslice": {types.NewListType(cel.StringType), func(attr resourceapi.NamedResourcesAttribute) any { "stringslice": {types.NewListType(cel.StringType), func(attr resourceapi.NamedResourcesAttribute) (any, error) {
if attr.StringSliceValue == nil { if attr.StringSliceValue == nil {
return nil return nil, nil
} }
return attr.StringSliceValue.Strings return attr.StringSliceValue.Strings, nil
}},
"version": {SemverType, func(attr resourceapi.NamedResourcesAttribute) (any, error) {
if attr.VersionValue == nil {
return nil, nil
}
v, err := semver.Parse(*attr.VersionValue)
if err != nil {
return nil, fmt.Errorf("parse semantic version: %v", err)
}
return Semver{Version: v}, nil
}}, }},
} }
@ -150,7 +162,11 @@ var boolType = reflect.TypeOf(true)
func (c CompilationResult) Evaluate(ctx context.Context, attributes []resourceapi.NamedResourcesAttribute) (bool, error) { func (c CompilationResult) Evaluate(ctx context.Context, attributes []resourceapi.NamedResourcesAttribute) (bool, error) {
variables := make(map[string]any, len(valueTypes)) variables := make(map[string]any, len(valueTypes))
for name, valueType := range valueTypes { for name, valueType := range valueTypes {
variables[attributesVarPrefix+name] = buildValueMapper(c.Environment.CELTypeAdapter(), attributes, valueType.get) m, err := buildValueMapper(c.Environment.CELTypeAdapter(), attributes, valueType.get)
if err != nil {
return false, fmt.Errorf("extract attributes with type %s: %v", name, err)
}
variables[attributesVarPrefix+name] = m
} }
result, _, err := c.Program.ContextEval(ctx, variables) result, _, err := c.Program.ContextEval(ctx, variables)
if err != nil { if err != nil {
@ -172,7 +188,9 @@ func mustBuildEnv() *environment.EnvSet {
versioned := []environment.VersionedOptions{ versioned := []environment.VersionedOptions{
{ {
IntroducedVersion: version.MajorMinor(1, 30), IntroducedVersion: version.MajorMinor(1, 30),
EnvOptions: buildVersionedAttributes(), EnvOptions: append(buildVersionedAttributes(),
SemverLib(),
),
}, },
} }
envset, err := envset.Extend(versioned...) envset, err := envset.Extend(versioned...)
@ -190,17 +208,21 @@ func buildVersionedAttributes() []cel.EnvOption {
return options return options
} }
func buildValueMapper(adapter types.Adapter, attributes []resourceapi.NamedResourcesAttribute, get func(resourceapi.NamedResourcesAttribute) any) traits.Mapper { func buildValueMapper(adapter types.Adapter, attributes []resourceapi.NamedResourcesAttribute, get func(resourceapi.NamedResourcesAttribute) (any, error)) (traits.Mapper, error) {
// This implementation constructs a map and then let's cel handle the // This implementation constructs a map and then let's cel handle the
// lookup and iteration. This is done for the sake of simplicity. // lookup and iteration. This is done for the sake of simplicity.
// Whether it's faster than writing a custom mapper depends on // Whether it's faster than writing a custom mapper depends on
// real-world attribute sets and CEL expressions and would have to be // real-world attribute sets and CEL expressions and would have to be
// benchmarked. // benchmarked.
valueMap := make(map[string]any) valueMap := make(map[string]any)
for _, attribute := range attributes { for name, attribute := range attributes {
if value := get(attribute); value != nil { value, err := get(attribute)
if err != nil {
return nil, fmt.Errorf("attribute %q: %v", name, err)
}
if value != nil {
valueMap[attribute.Name] = value valueMap[attribute.Name] = value
} }
} }
return types.NewStringInterfaceMap(adapter, valueMap) return types.NewStringInterfaceMap(adapter, valueMap), nil
} }

View File

@ -0,0 +1,73 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cel
import (
"fmt"
"reflect"
"github.com/blang/semver/v4"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
)
var (
SemverType = cel.ObjectType("kubernetes.Semver")
)
// Semver provdes a CEL representation of a [semver.Version].
type Semver struct {
semver.Version
}
func (v Semver) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
if reflect.TypeOf(v.Version).AssignableTo(typeDesc) {
return v.Version, nil
}
if reflect.TypeOf("").AssignableTo(typeDesc) {
return v.Version.String(), nil
}
return nil, fmt.Errorf("type conversion error from 'Semver' to '%v'", typeDesc)
}
func (v Semver) ConvertToType(typeVal ref.Type) ref.Val {
switch typeVal {
case SemverType:
return v
case types.TypeType:
return SemverType
default:
return types.NewErr("type conversion error from '%s' to '%s'", SemverType, typeVal)
}
}
func (v Semver) Equal(other ref.Val) ref.Val {
otherDur, ok := other.(Semver)
if !ok {
return types.MaybeNoSuchOverloadErr(other)
}
return types.Bool(v.Version.EQ(otherDur.Version))
}
func (v Semver) Type() ref.Type {
return SemverType
}
func (v Semver) Value() interface{} {
return v.Version
}

View File

@ -0,0 +1,209 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cel_test
import (
"regexp"
"testing"
"github.com/blang/semver/v4"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/util/sets"
library "k8s.io/dynamic-resource-allocation/structured/namedresources/cel"
)
func testSemver(t *testing.T, expr string, expectResult ref.Val, expectRuntimeErrPattern string, expectCompileErrs []string) {
env, err := cel.NewEnv(
library.SemverLib(),
)
if err != nil {
t.Fatalf("%v", err)
}
compiled, issues := env.Compile(expr)
if len(expectCompileErrs) > 0 {
missingCompileErrs := []string{}
matchedCompileErrs := sets.New[int]()
for _, expectedCompileErr := range expectCompileErrs {
compiledPattern, err := regexp.Compile(expectedCompileErr)
if err != nil {
t.Fatalf("failed to compile expected err regex: %v", err)
}
didMatch := false
for i, compileError := range issues.Errors() {
if compiledPattern.Match([]byte(compileError.Message)) {
didMatch = true
matchedCompileErrs.Insert(i)
}
}
if !didMatch {
missingCompileErrs = append(missingCompileErrs, expectedCompileErr)
} else if len(matchedCompileErrs) != len(issues.Errors()) {
unmatchedErrs := []cel.Error{}
for i, issue := range issues.Errors() {
if !matchedCompileErrs.Has(i) {
unmatchedErrs = append(unmatchedErrs, *issue)
}
}
require.Empty(t, unmatchedErrs, "unexpected compilation errors")
}
}
require.Empty(t, missingCompileErrs, "expected compilation errors")
return
} else if len(issues.Errors()) > 0 {
for _, err := range issues.Errors() {
t.Errorf("unexpected compile error: %v", err)
}
t.FailNow()
}
prog, err := env.Program(compiled)
if err != nil {
t.Fatalf("%v", err)
}
res, _, err := prog.Eval(map[string]interface{}{})
if len(expectRuntimeErrPattern) > 0 {
if err == nil {
t.Fatalf("no runtime error thrown. Expected: %v", expectRuntimeErrPattern)
} else if matched, regexErr := regexp.MatchString(expectRuntimeErrPattern, err.Error()); regexErr != nil {
t.Fatalf("failed to compile expected err regex: %v", regexErr)
} else if !matched {
t.Fatalf("unexpected err: %v", err)
}
} else if err != nil {
t.Fatalf("%v", err)
} else if expectResult != nil {
converted := res.Equal(expectResult).Value().(bool)
require.True(t, converted, "expectation not equal to output")
} else {
t.Fatal("expected result must not be nil")
}
}
func TestSemver(t *testing.T) {
trueVal := types.Bool(true)
falseVal := types.Bool(false)
cases := []struct {
name string
expr string
expectValue ref.Val
expectedCompileErr []string
expectedRuntimeErr string
}{
{
name: "parse",
expr: `semver("1.2.3")`,
expectValue: library.Semver{Version: semver.MustParse("1.2.3")},
},
{
name: "parseInvalidVersion",
expr: `semver("v1.0")`,
expectedRuntimeErr: "No Major.Minor.Patch elements found",
},
{
name: "isSemver",
expr: `isSemver("1.2.3-beta.1+build.1")`,
expectValue: trueVal,
},
{
name: "isSemver_false",
expr: `isSemver("v1.0")`,
expectValue: falseVal,
},
{
name: "isSemver_noOverload",
expr: `isSemver([1, 2, 3])`,
expectedCompileErr: []string{"found no matching overload for 'isSemver' applied to.*"},
},
{
name: "equality_reflexivity",
expr: `semver("1.2.3") == semver("1.2.3")`,
expectValue: trueVal,
},
{
name: "inequality",
expr: `semver("1.2.3") == semver("1.0.0")`,
expectValue: falseVal,
},
{
name: "semver_less",
expr: `semver("1.0.0").isLessThan(semver("1.2.3"))`,
expectValue: trueVal,
},
{
name: "semver_less_false",
expr: `semver("1.0.0").isLessThan(semver("1.0.0"))`,
expectValue: falseVal,
},
{
name: "semver_greater",
expr: `semver("1.2.3").isGreaterThan(semver("1.0.0"))`,
expectValue: trueVal,
},
{
name: "semver_greater_false",
expr: `semver("1.0.0").isGreaterThan(semver("1.0.0"))`,
expectValue: falseVal,
},
{
name: "compare_equal",
expr: `semver("1.2.3").compareTo(semver("1.2.3"))`,
expectValue: types.Int(0),
},
{
name: "compare_less",
expr: `semver("1.0.0").compareTo(semver("1.2.3"))`,
expectValue: types.Int(-1),
},
{
name: "compare_greater",
expr: `semver("1.2.3").compareTo(semver("1.0.0"))`,
expectValue: types.Int(1),
},
{
name: "major",
expr: `semver("1.2.3").major()`,
expectValue: types.Int(1),
},
{
name: "minor",
expr: `semver("1.2.3").minor()`,
expectValue: types.Int(2),
},
{
name: "patch",
expr: `semver("1.2.3").patch()`,
expectValue: types.Int(3),
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
testSemver(t, c.expr, c.expectValue, c.expectedRuntimeErr, c.expectedCompileErr)
})
}
}

View File

@ -0,0 +1,238 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cel
import (
"github.com/blang/semver/v4"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
)
// Semver provides a CEL function library extension for [semver.Version].
//
// semver
//
// Converts a string to a semantic version or results in an error if the string is not a valid semantic version. Refer
// to semver.org documentation for information on accepted patterns.
//
// semver(<string>) <Semver>
//
// Examples:
//
// semver('1.0.0') // returns a Semver
// semver('0.1.0-alpha.1') // returns a Semver
// semver('200K') // error
// semver('Three') // error
// semver('Mi') // error
//
// isSemver
//
// Returns true if a string is a valid Semver. isSemver returns true if and
// only if semver does not result in error.
//
// isSemver( <string>) <bool>
//
// Examples:
//
// isSemver('1.0.0') // returns true
// isSemver('v1.0') // returns true (tolerant parsing)
// isSemver('hello') // returns false
//
// Conversion to Scalars:
//
// - major/minor/patch: return the major version number as int64.
//
// <Semver>.major() <int>
//
// Examples:
//
// semver("1.2.3").major() // returns 1
//
// Comparisons
//
// - isGreaterThan: Returns true if and only if the receiver is greater than the operand
//
// - isLessThan: Returns true if and only if the receiver is less than the operand
//
// - compareTo: Compares receiver to operand and returns 0 if they are equal, 1 if the receiver is greater, or -1 if the receiver is less than the operand
//
//
// <Semver>.isLessThan(<semver>) <bool>
// <Semver>.isGreaterThan(<semver>) <bool>
// <Semver>.compareTo(<semver>) <int>
//
// Examples:
//
// semver("1.2.3").compareTo(semver("1.2.3")) // returns 0
// semver("1.2.3").compareTo(semver("2.0.0")) // returns -1
// semver("1.2.3").compareTo(semver("0.1.2")) // returns 1
func SemverLib() cel.EnvOption {
return cel.Lib(semverLib)
}
var semverLib = &semverLibType{}
type semverLibType struct{}
func (*semverLibType) LibraryName() string {
return "k8s.semver"
}
func (*semverLibType) CompileOptions() []cel.EnvOption {
// Defined in this function to avoid an initialization order problem.
semverLibraryDecls := map[string][]cel.FunctionOpt{
"semver": {
cel.Overload("string_to_semver", []*cel.Type{cel.StringType}, SemverType, cel.UnaryBinding((stringToSemver))),
},
"isSemver": {
cel.Overload("is_semver_string", []*cel.Type{cel.StringType}, cel.BoolType, cel.UnaryBinding(isSemver)),
},
"isGreaterThan": {
cel.MemberOverload("semver_is_greater_than", []*cel.Type{SemverType, SemverType}, cel.BoolType, cel.BinaryBinding(semverIsGreaterThan)),
},
"isLessThan": {
cel.MemberOverload("semver_is_less_than", []*cel.Type{SemverType, SemverType}, cel.BoolType, cel.BinaryBinding(semverIsLessThan)),
},
"compareTo": {
cel.MemberOverload("semver_compare_to", []*cel.Type{SemverType, SemverType}, cel.IntType, cel.BinaryBinding(semverCompareTo)),
},
"major": {
cel.MemberOverload("semver_major", []*cel.Type{SemverType}, cel.IntType, cel.UnaryBinding(semverMajor)),
},
"minor": {
cel.MemberOverload("semver_minor", []*cel.Type{SemverType}, cel.IntType, cel.UnaryBinding(semverMinor)),
},
"patch": {
cel.MemberOverload("semver_patch", []*cel.Type{SemverType}, cel.IntType, cel.UnaryBinding(semverPatch)),
},
}
options := make([]cel.EnvOption, 0, len(semverLibraryDecls))
for name, overloads := range semverLibraryDecls {
options = append(options, cel.Function(name, overloads...))
}
return options
}
func (*semverLibType) ProgramOptions() []cel.ProgramOption {
return []cel.ProgramOption{}
}
func isSemver(arg ref.Val) ref.Val {
str, ok := arg.Value().(string)
if !ok {
return types.MaybeNoSuchOverloadErr(arg)
}
// Using semver/v4 here is okay because this function isn't
// used to validate the Kubernetes API. In the CEL base library
// we would have to use the regular expression from
// pkg/apis/resource/structured/namedresources/validation/validation.go.
_, err := semver.Parse(str)
if err != nil {
return types.Bool(false)
}
return types.Bool(true)
}
func stringToSemver(arg ref.Val) ref.Val {
str, ok := arg.Value().(string)
if !ok {
return types.MaybeNoSuchOverloadErr(arg)
}
// Using semver/v4 here is okay because this function isn't
// used to validate the Kubernetes API. In the CEL base library
// we would have to use the regular expression from
// pkg/apis/resource/structured/namedresources/validation/validation.go
// first before parsing.
v, err := semver.Parse(str)
if err != nil {
return types.WrapErr(err)
}
return Semver{Version: v}
}
func semverMajor(arg ref.Val) ref.Val {
v, ok := arg.Value().(semver.Version)
if !ok {
return types.MaybeNoSuchOverloadErr(arg)
}
return types.Int(v.Major)
}
func semverMinor(arg ref.Val) ref.Val {
v, ok := arg.Value().(semver.Version)
if !ok {
return types.MaybeNoSuchOverloadErr(arg)
}
return types.Int(v.Minor)
}
func semverPatch(arg ref.Val) ref.Val {
v, ok := arg.Value().(semver.Version)
if !ok {
return types.MaybeNoSuchOverloadErr(arg)
}
return types.Int(v.Patch)
}
func semverIsGreaterThan(arg ref.Val, other ref.Val) ref.Val {
v, ok := arg.Value().(semver.Version)
if !ok {
return types.MaybeNoSuchOverloadErr(arg)
}
v2, ok := other.Value().(semver.Version)
if !ok {
return types.MaybeNoSuchOverloadErr(arg)
}
return types.Bool(v.Compare(v2) == 1)
}
func semverIsLessThan(arg ref.Val, other ref.Val) ref.Val {
v, ok := arg.Value().(semver.Version)
if !ok {
return types.MaybeNoSuchOverloadErr(arg)
}
v2, ok := other.Value().(semver.Version)
if !ok {
return types.MaybeNoSuchOverloadErr(arg)
}
return types.Bool(v.Compare(v2) == -1)
}
func semverCompareTo(arg ref.Val, other ref.Val) ref.Val {
v, ok := arg.Value().(semver.Version)
if !ok {
return types.MaybeNoSuchOverloadErr(arg)
}
v2, ok := other.Value().(semver.Version)
if !ok {
return types.MaybeNoSuchOverloadErr(arg)
}
return types.Int(v.Compare(v2))
}