This repository has been archived by the owner on Aug 12, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 50
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
7 changed files
with
1,006 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]) | ||
} | ||
} |
Oops, something went wrong.