Skip to content

Commit

Permalink
cel: add semantic version type
Browse files Browse the repository at this point in the history
This is derived from the quantity type and supports the same operations, with
one exception: comparison across types (like <semver> == <int>) is not
supported.
  • Loading branch information
pohly committed Mar 4, 2024
1 parent e798fa6 commit 4697699
Show file tree
Hide file tree
Showing 4 changed files with 314 additions and 1 deletion.
2 changes: 1 addition & 1 deletion staging/src/k8s.io/apiserver/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module k8s.io/apiserver
go 1.22.0

require (
github.com/blang/semver/v4 v4.0.0
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/coreos/go-systemd/v22 v22.5.0
github.com/emicklei/go-restful/v3 v3.11.0
Expand Down Expand Up @@ -61,7 +62,6 @@ require (
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
Expand Down
7 changes: 7 additions & 0 deletions staging/src/k8s.io/apiserver/pkg/cel/environment/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ var baseOpts = []VersionedOptions{
library.Quantity(),
},
},
{
IntroducedVersion: version.MajorMinor(1, 30),
EnvOptions: []cel.EnvOption{
cel.OptionalTypes(), // Copied from library.Quantity entry above, NOP?
library.SemVer(),
},
},
// add the new validator in 1.29
{
IntroducedVersion: version.MajorMinor(1, 29),
Expand Down
230 changes: 230 additions & 0 deletions staging/src/k8s.io/apiserver/pkg/cel/library/semver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
/*
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 library

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"

apiservercel "k8s.io/apiserver/pkg/cel"
)

// SemVer provides a CEL function library extension for [semver.Version].
//
// semver
//
// Converts a string to a SemVer or results in an error if the string is not a valid SemVer. Refer
// to semver.SemVer 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 SemVer() cel.EnvOption {
return cel.Lib(semverLib)
}

var semverLib = &semverLibType{}

type semverLibType struct{}

func (*semverLibType) LibraryName() string {
return "k8s.semver"
}

var semverLibraryDecls = map[string][]cel.FunctionOpt{
"semver": {
cel.Overload("string_to_semver", []*cel.Type{cel.StringType}, apiservercel.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{apiservercel.SemVerType, apiservercel.SemVerType}, cel.BoolType, cel.BinaryBinding(semverIsGreaterThan)),
},
"isLessThan": {
cel.MemberOverload("semver_is_less_than", []*cel.Type{apiservercel.SemVerType, apiservercel.SemVerType}, cel.BoolType, cel.BinaryBinding(semverIsLessThan)),
},
"compareTo": {
cel.MemberOverload("semver_compare_to", []*cel.Type{apiservercel.SemVerType, apiservercel.SemVerType}, cel.IntType, cel.BinaryBinding(semverCompareTo)),
},
"major": {
cel.MemberOverload("semver_major", []*cel.Type{apiservercel.SemVerType}, cel.IntType, cel.UnaryBinding(semverMajor)),
},
"minor": {
cel.MemberOverload("semver_minor", []*cel.Type{apiservercel.SemVerType}, cel.IntType, cel.UnaryBinding(semverMinor)),
},
"patch": {
cel.MemberOverload("semver_patch", []*cel.Type{apiservercel.SemVerType}, cel.IntType, cel.UnaryBinding(semverPatch)),
},
}

func (*semverLibType) CompileOptions() []cel.EnvOption {
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)
}

_, 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)
}

v, err := semver.Parse(str)
if err != nil {
return types.WrapErr(err)
}

return apiservercel.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))
}
76 changes: 76 additions & 0 deletions staging/src/k8s.io/apiserver/pkg/cel/semver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
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

import (
"fmt"
"reflect"

"github.com/blang/semver/v4"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/checker/decls"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
)

var (
SemVerObject = decls.NewObjectType("kubernetes.SemVer")
semVerTypeValue = types.NewTypeValue("kubernetes.SemVer")
SemVerType = cel.ObjectType("kubernetes.SemVer")
)

// SemVer provdes a CEL representation of a [semver.SemVer].
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 typeValue:
return v
case types.TypeType:
return semVerTypeValue
default:
return types.NewErr("type conversion error from '%s' to '%s'", semVerTypeValue, 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 semVerTypeValue
}

func (v SemVer) Value() interface{} {
return v.Version
}

0 comments on commit 4697699

Please sign in to comment.