Skip to content
This repository has been archived by the owner on Aug 12, 2024. It is now read-only.

Commit

Permalink
pull in probing package from PKO
Browse files Browse the repository at this point in the history
Signed-off-by: Per Goncalves da Silva <[email protected]>
  • Loading branch information
Per Goncalves da Silva committed Oct 10, 2023
1 parent e92e481 commit 4f57e9d
Show file tree
Hide file tree
Showing 7 changed files with 1,006 additions and 0 deletions.
65 changes: 65 additions & 0 deletions internal/probing/cel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package probing

import (
"errors"
"fmt"

"github.com/google/cel-go/cel"
"github.com/google/cel-go/ext"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apiserver/pkg/cel/library"
)

type celProbe struct {
Program cel.Program
Message string
}

var ErrCELInvalidEvaluationType = errors.New(
"cel expression must evaluate to a bool")

func newCELProbe(rule, message string) (*celProbe, error) {
env, err := cel.NewEnv(
cel.Variable("self", cel.DynType),
cel.HomogeneousAggregateLiterals(),
cel.EagerlyValidateDeclarations(true),
cel.DefaultUTCTimeZone(true),

ext.Strings(ext.StringsVersion(0)),
library.URLs(),
library.Regex(),
library.Lists(),
)
if err != nil {
return nil, fmt.Errorf("creating CEL env: %w", err)
}

ast, issues := env.Compile(rule)
if issues != nil {
return nil, fmt.Errorf("compiling CEL: %w", issues.Err())
}
if ast.OutputType() != cel.BoolType {
return nil, ErrCELInvalidEvaluationType
}

prgm, err := env.Program(ast)
if err != nil {
return nil, fmt.Errorf("CEL program failed: %w", err)
}

return &celProbe{
Program: prgm,
Message: message,
}, nil
}

func (p *celProbe) Probe(obj *unstructured.Unstructured) (success bool, message string) {
val, _, err := p.Program.Eval(map[string]any{
"self": obj.Object,
})
if err != nil {
return false, fmt.Sprintf("CEL program failed: %v", err)
}

return val.Value().(bool), p.Message
}
124 changes: 124 additions & 0 deletions internal/probing/cel_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package probing

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

func Test_newCELProbe(t *testing.T) {
t.Parallel()

_, err := newCELProbe(`self.test`, "")
require.ErrorIs(t, err, ErrCELInvalidEvaluationType)
}

func Test_celProbe(t *testing.T) {
t.Parallel()
tests := []struct {
name string
rule, message string
obj *unstructured.Unstructured

success bool
}{
{
name: "simple success",
rule: `self.metadata.name == "hans"`,
message: "aaaaaah!",
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "hans",
},
},
},
success: true,
},
{
name: "simple failure",
rule: `self.metadata.name == "hans"`,
message: "aaaaaah!",
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "nothans",
},
},
},
success: false,
},
{
name: "OpenShift Route success simple",
rule: `self.status.ingress.all(i, i.conditions.all(c, c.type == "Ready" && c.status == "True"))`,
message: "aaaaaah!",
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"status": map[string]interface{}{
"test": []interface{}{"1", "2", "3"},
"ingress": []interface{}{
map[string]interface{}{
"host": "hostname.xxx.xxx",
"conditions": []interface{}{
map[string]interface{}{
"type": "Ready",
"status": "True",
},
},
},
},
},
},
},
success: true,
},
{
name: "OpenShift Route failure",
rule: `self.status.ingress.all(i, i.conditions.all(c, c.type == "Ready" && c.status == "True"))`,
message: "aaaaaah!",
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"status": map[string]interface{}{
"test": []interface{}{"1", "2", "3"},
"ingress": []interface{}{
map[string]interface{}{
"host": "hostname.xxx.xxx",
"conditions": []interface{}{
map[string]interface{}{
"type": "Ready",
"status": "True",
},
},
},
map[string]interface{}{
"host": "otherhost.xxx.xxx",
"conditions": []interface{}{
map[string]interface{}{
"type": "Ready",
"status": "False",
},
},
},
},
},
},
},
success: false,
},
}

for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
p, err := newCELProbe(test.rule, test.message)
require.NoError(t, err)

success, outMsg := p.Probe(test.obj)
assert.Equal(t, test.success, success)
assert.Equal(t, test.message, outMsg)
})
}
}
99 changes: 99 additions & 0 deletions internal/probing/parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package probing

import (
"context"
"fmt"
rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
)

// Parse takes a list of ObjectSetProbes (commonly defined within a ObjectSetPhaseSpec)
// and compiles a single Prober to test objects with.
func Parse(ctx context.Context, bdProbes []rukpakv1alpha1.BundleDeploymentProbe) (Prober, error) {
probeList := make(list, len(bdProbes))
for i, bdProbe := range bdProbes {
var (
probe Prober
err error
)
probe, err = ParseProbes(ctx, bdProbe.Probes)
if err != nil {
return nil, fmt.Errorf("parsing probe #%d: %w", i, err)
}
probe, err = ParseSelector(ctx, bdProbe.Selector, probe)
if err != nil {
return nil, fmt.Errorf("parsing selector of probe #%d: %w", i, err)
}
probeList[i] = probe
}
return probeList, nil
}

// ParseSelector reads a corev1alpha1.ProbeSelector and wraps a Prober,
// only executing the Prober when the selector criteria match.
func ParseSelector(_ context.Context, selector rukpakv1alpha1.ProbeSelector, probe Prober) (Prober, error) {
if selector.Kind != nil {
probe = &kindSelector{
Prober: probe,
GroupKind: schema.GroupKind{
Group: selector.Kind.Group,
Kind: selector.Kind.Kind,
},
}
}
if selector.Selector != nil {
s, err := metav1.LabelSelectorAsSelector(selector.Selector)
if err != nil {
return nil, err
}
probe = &selectorSelector{
Prober: probe,
Selector: s,
}
}
return probe, nil
}

// ParseProbes takes a []corev1alpha1.Probe and compiles it into a Prober.
func ParseProbes(_ context.Context, probeSpecs []rukpakv1alpha1.Probe) (Prober, error) {
var probeList list
for _, probeSpec := range probeSpecs {
var (
probe Prober
err error
)

switch {
case probeSpec.FieldsEqual != nil:
probe = &fieldsEqualProbe{
FieldA: probeSpec.FieldsEqual.FieldA,
FieldB: probeSpec.FieldsEqual.FieldB,
}

case probeSpec.Condition != nil:
probe = NewConditionProbe(
probeSpec.Condition.Type,
probeSpec.Condition.Status,
)

case probeSpec.CEL != nil:
probe, err = newCELProbe(
probeSpec.CEL.Rule,
probeSpec.CEL.Message,
)
if err != nil {
return nil, err
}

default:
// probe has no known config
continue
}
probeList = append(probeList, probe)
}

// Always check .status.observedCondition, if present.
return &statusObservedGenerationProbe{Prober: probeList}, nil
}
107 changes: 107 additions & 0 deletions internal/probing/parse_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package probing

import (
"context"
rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestParse(t *testing.T) {
t.Parallel()
ctx := context.Background()
kind := "Test"
group := "test-group"
osp := []rukpakv1alpha1.BundleDeploymentProbe{
{
Selector: rukpakv1alpha1.ProbeSelector{
Kind: &rukpakv1alpha1.BundleDeploymentProbeKindSpec{
Kind: kind,
Group: group,
},
},
},
}

p, err := Parse(ctx, osp)
require.NoError(t, err)
require.IsType(t, list{}, p)

if assert.Len(t, p, 1) {
list := p.(list)
require.IsType(t, &kindSelector{}, list[0])
ks := list[0].(*kindSelector)
assert.Equal(t, kind, ks.Kind)
assert.Equal(t, group, ks.Group)
}
}

func TestParseSelector(t *testing.T) {
t.Parallel()
ctx := context.Background()
p, err := ParseSelector(ctx, rukpakv1alpha1.ProbeSelector{
Kind: &rukpakv1alpha1.BundleDeploymentProbeKindSpec{
Kind: "Test",
Group: "test",
},
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"test": "test123",
},
},
}, nil)
require.NoError(t, err)
require.IsType(t, &selectorSelector{}, p)

ss := p.(*selectorSelector)
require.IsType(t, &kindSelector{}, ss.Prober)
}

func TestParseProbes(t *testing.T) {
t.Parallel()
fep := rukpakv1alpha1.Probe{
FieldsEqual: &rukpakv1alpha1.ProbeFieldsEqualSpec{
FieldA: "asdf",
FieldB: "jkl;",
},
}
cp := rukpakv1alpha1.Probe{
Condition: &rukpakv1alpha1.ProbeConditionSpec{
Type: "asdf",
Status: "asdf",
},
}
cel := rukpakv1alpha1.Probe{
CEL: &rukpakv1alpha1.ProbeCELSpec{
Message: "test",
Rule: `self.metadata.name == "test"`,
},
}
emptyConfigProbe := rukpakv1alpha1.Probe{}

p, err := ParseProbes(context.Background(), []rukpakv1alpha1.Probe{
fep, cp, cel, emptyConfigProbe,
})
require.NoError(t, err)
// everything should be wrapped
require.IsType(t, &statusObservedGenerationProbe{}, p)

ogProbe := p.(*statusObservedGenerationProbe)
nested := ogProbe.Prober
require.IsType(t, list{}, nested)

if assert.Len(t, nested, 3) {
nestedList := nested.(list)
assert.Equal(t, &fieldsEqualProbe{
FieldA: "asdf",
FieldB: "jkl;",
}, nestedList[0])
assert.Equal(t, &conditionProbe{
Type: "asdf",
Status: "asdf",
}, nestedList[1])
}
}
Loading

0 comments on commit 4f57e9d

Please sign in to comment.