Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,11 +273,13 @@ Any check not explicitly mocked returns "NoOpinion".

### Operations

- **CREATE** (default): Provide `.object.yaml` (or `object` in `.request.yaml`).
- **UPDATE**: Provide both `.object.yaml` (new) and `.oldObject.yaml` (old). Operation is inferred automatically.
- **DELETE**: Provide only `.oldObject.yaml`. Operation is inferred automatically.
- **CREATE** (default): Provide `object` (via `.object.yaml` or in `.request.yaml`).
- **UPDATE**: Provide both `object` and `oldObject`. Operation is inferred automatically.
- **DELETE**: Provide only `oldObject`. Operation is inferred automatically.
- **CONNECT**: Set `operation: CONNECT` in `.request.yaml`.

Operation inference works the same way whether fields are in separate files or consolidated in `.request.yaml`. If you set `operation:` explicitly and it conflicts with what would be inferred from the fields present, `kat` reports an error.

## Features

- **Standard Kubernetes YAML** — no new DSL to learn
Expand Down
18 changes: 9 additions & 9 deletions internal/evaluator/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,12 +353,12 @@ func checkAuditAnnotations(expected *TestExpectation, actual *TestOutcome) *Test

expectedYAML, err := yaml.Marshal(expected.AuditAnnotations)
if err != nil {
expectedYAML = []byte(fmt.Sprintf("%+v", expected.AuditAnnotations))
expectedYAML = fmt.Appendf(nil, "%+v", expected.AuditAnnotations)
}

actualYAML, err := yaml.Marshal(actualFiltered)
if err != nil {
actualYAML = []byte(fmt.Sprintf("%+v", actualFiltered))
actualYAML = fmt.Appendf(nil, "%+v", actualFiltered)
}

diff := getDiff(string(expectedYAML), string(actualYAML))
Expand Down Expand Up @@ -397,12 +397,12 @@ func checkMutatedObject(expected *TestExpectation, actual *TestOutcome) *TestRes
// Convert to YAML for consistent diffing
expectedYAML, err := yaml.Marshal(expected.Object.Object)
if err != nil {
expectedYAML = []byte(fmt.Sprintf("%+v", expected.Object.Object))
expectedYAML = fmt.Appendf(nil, "%+v", expected.Object.Object)
}

actualYAML, err := yaml.Marshal(actual.Object.Object)
if err != nil {
actualYAML = []byte(fmt.Sprintf("%+v", actual.Object.Object))
actualYAML = fmt.Appendf(nil, "%+v", actual.Object.Object)
}

// Generate a standard unified diff
Expand Down Expand Up @@ -1089,7 +1089,7 @@ func appendPatchOperations(iter traits.Iterator, result *jsonpatch.Patch) error
}

func buildJSONPatchOperation(value ref.Val) (jsonpatch.Operation, error) {
patchObj, err := value.ConvertToNative(reflect.TypeOf(&mutation.JSONPatchVal{}))
patchObj, err := value.ConvertToNative(reflect.TypeFor[*mutation.JSONPatchVal]())
if err != nil {
return nil, fmt.Errorf("convert patch element: %w", err)
}
Expand All @@ -1100,15 +1100,15 @@ func buildJSONPatchOperation(value ref.Val) (jsonpatch.Operation, error) {
}

resultOp := jsonpatch.Operation{}
resultOp["op"] = ptr.To(json.RawMessage(strconv.Quote(op.Op)))
resultOp["path"] = ptr.To(json.RawMessage(strconv.Quote(op.Path)))
resultOp["op"] = new(json.RawMessage(strconv.Quote(op.Op)))
resultOp["path"] = new(json.RawMessage(strconv.Quote(op.Path)))

if len(op.From) > 0 {
resultOp["from"] = ptr.To(json.RawMessage(strconv.Quote(op.From)))
resultOp["from"] = new(json.RawMessage(strconv.Quote(op.From)))
}

if op.Val != nil {
converted, err := op.Val.ConvertToNative(reflect.TypeOf(&structpb.Value{}))
converted, err := op.Val.ConvertToNative(reflect.TypeFor[*structpb.Value]())
if err != nil {
return nil, fmt.Errorf("convert patch value: %w", err)
}
Expand Down
16 changes: 8 additions & 8 deletions internal/evaluator/evaluator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1544,10 +1544,10 @@ func TestEvaluator_EvaluateTest(t *testing.T) {
}

validPod := &unstructured.Unstructured{
Object: map[string]interface{}{
Object: map[string]any{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]interface{}{
"metadata": map[string]any{
"name": "test-pod",
"namespace": "default",
},
Expand Down Expand Up @@ -1646,13 +1646,13 @@ func TestEvaluator_EvaluateTest(t *testing.T) {
Object: validPod,
ExpectAllowed: true,
ExpectedObject: &unstructured.Unstructured{
Object: map[string]interface{}{
Object: map[string]any{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]interface{}{
"metadata": map[string]any{
"name": "test-pod",
"namespace": "default",
"labels": map[string]interface{}{
"labels": map[string]any{
"foo": "bar",
},
},
Expand All @@ -1679,13 +1679,13 @@ func TestEvaluator_EvaluateTest(t *testing.T) {
Object: validPod,
ExpectAllowed: true,
ExpectedObject: &unstructured.Unstructured{ // Expect different label
Object: map[string]interface{}{
Object: map[string]any{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]interface{}{
"metadata": map[string]any{
"name": "test-pod",
"namespace": "default",
"labels": map[string]interface{}{
"labels": map[string]any{
"foo": "baz",
},
},
Expand Down
1 change: 1 addition & 0 deletions internal/loader/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ var (
ErrConflictOldObject = errors.New("conflict: oldObject defined in multiple files")
ErrConflictNamespaceObject = errors.New("conflict: namespaceObject defined in multiple files")
ErrConflictParams = errors.New("conflict: params defined in multiple files")
ErrOperationMismatch = errors.New("explicit operation does not match inferred operation")
)
50 changes: 25 additions & 25 deletions internal/loader/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ type simplifiedRequest struct {
SubResource string `json:"subResource,omitempty"`
Name string `json:"name,omitempty"`
Namespace string `json:"namespace,omitempty"`
NamespaceObject map[string]interface{} `json:"namespaceObject,omitempty"`
NamespaceObject map[string]any `json:"namespaceObject,omitempty"`
UserInfo *authenticationv1.UserInfo `json:"userInfo,omitempty"`
Object map[string]interface{} `json:"object,omitempty"`
OldObject map[string]interface{} `json:"oldObject,omitempty"`
Params map[string]interface{} `json:"params,omitempty"`
Options map[string]interface{} `json:"options,omitempty"`
Object map[string]any `json:"object,omitempty"`
OldObject map[string]any `json:"oldObject,omitempty"`
Params map[string]any `json:"params,omitempty"`
Options map[string]any `json:"options,omitempty"`
}

// parseRequestYAML parses a simplified request format.
Expand Down Expand Up @@ -131,7 +131,7 @@ func validateSimplifiedRequest(req *simplifiedRequest) error {
return nil
}

func validateWithScheme(obj map[string]interface{}, field string, expectedGVK *schema.GroupVersionKind) error {
func validateWithScheme(obj map[string]any, field string, expectedGVK *schema.GroupVersionKind) error {
if obj == nil {
return nil
}
Expand All @@ -149,7 +149,7 @@ func validateWithScheme(obj map[string]interface{}, field string, expectedGVK *s
return validateStructureStrict(obj, field, expectedGVK, u.GetKind())
}

func validateStructureStrict(obj map[string]interface{}, field string, expectedGVK *schema.GroupVersionKind, currentKind string) error {
func validateStructureStrict(obj map[string]any, field string, expectedGVK *schema.GroupVersionKind, currentKind string) error {
// Decode using the default Kubernetes scheme to strictly validate known types
data, err := json.Marshal(obj)
if err != nil {
Expand Down Expand Up @@ -235,7 +235,7 @@ func buildAdmissionRequestFromSimplified(req *simplifiedRequest, testReq *testRe

// parseObjectYAML parses a raw Kubernetes object and creates an AdmissionRequest for it.
func parseObjectYAML(testReq *testRequest, data []byte) error {
var obj map[string]interface{}
var obj map[string]any
if err := yaml.Unmarshal(data, &obj); err != nil {
return fmt.Errorf("failed to unmarshal object: %w", err)
}
Expand All @@ -246,7 +246,7 @@ func parseObjectYAML(testReq *testRequest, data []byte) error {

unstruct := &unstructured.Unstructured{Object: obj}
testReq.Object = unstruct
testReq.Request = buildCreateRequestFromObject(testReq.Name, unstruct)
testReq.Request = buildRequestFromObject(testReq.Name, unstruct)
testReq.NamespaceName = unstruct.GetNamespace()

if err := loadAuxiliaryFiles(testReq); err != nil {
Expand All @@ -256,12 +256,13 @@ func parseObjectYAML(testReq *testRequest, data []byte) error {
return nil
}

func buildCreateRequestFromObject(testName string, obj *unstructured.Unstructured) *admissionv1.AdmissionRequest {
// buildRequestFromObject creates a partial AdmissionRequest with resource metadata.
// Operation is not set here — it is inferred centrally by inferOrValidateOperation.
func buildRequestFromObject(testName string, obj *unstructured.Unstructured) *admissionv1.AdmissionRequest {
gvk := obj.GroupVersionKind()

return &admissionv1.AdmissionRequest{
UID: types.UID("test-" + testName),
Operation: admissionv1.Create,
UID: types.UID("test-" + testName),
Kind: metav1.GroupVersionKind{
Group: gvk.Group,
Version: gvk.Version,
Expand Down Expand Up @@ -309,7 +310,7 @@ func loadGoldFile(testReq *testRequest) error {
return fmt.Errorf("failed to read gold file: %w", err)
}

var goldObj map[string]interface{}
var goldObj map[string]any
if err := yaml.Unmarshal(goldData, &goldObj); err != nil {
return fmt.Errorf("failed to unmarshal gold object: %w", err)
}
Expand Down Expand Up @@ -342,10 +343,11 @@ func loadMessageFile(testReq *testRequest) error {
return nil
}

// parseOldObjectYAML parses a raw Kubernetes object and creates an AdmissionRequest for DELETE operation.
// This is used for testing deletion policies where only oldObject is relevant.
// parseOldObjectYAML parses a raw Kubernetes object for the oldObject field.
// Operation is not set here — it is inferred centrally by inferOrValidateOperation
// (DELETE if only oldObject is present, UPDATE if both object and oldObject are present).
func parseOldObjectYAML(testReq *testRequest, data []byte) error {
var obj map[string]interface{}
var obj map[string]any
if err := yaml.Unmarshal(data, &obj); err != nil {
return fmt.Errorf("failed to unmarshal oldObject: %w", err)
}
Expand All @@ -357,11 +359,10 @@ func parseOldObjectYAML(testReq *testRequest, data []byte) error {
unstruct := &unstructured.Unstructured{Object: obj}
testReq.OldObject = unstruct

// Build AdmissionRequest for DELETE operation
// Build a partial AdmissionRequest with resource metadata (operation set later by inference).
gvk := unstruct.GroupVersionKind()
admReq := &admissionv1.AdmissionRequest{
UID: types.UID("test-" + testReq.Name),
Operation: admissionv1.Delete,
testReq.Request = &admissionv1.AdmissionRequest{
UID: types.UID("test-" + testReq.Name),
Kind: metav1.GroupVersionKind{
Group: gvk.Group,
Version: gvk.Version,
Expand All @@ -376,7 +377,6 @@ func parseOldObjectYAML(testReq *testRequest, data []byte) error {
Namespace: unstruct.GetNamespace(),
}

testReq.Request = admReq
testReq.NamespaceName = unstruct.GetNamespace()

// Look for corresponding .message.txt file (expected error message).
Expand All @@ -396,7 +396,7 @@ func parseOldObjectYAML(testReq *testRequest, data []byte) error {

// parseNamespaceObjectYAML parses a Kubernetes Namespace object providing namespace context for the admission request.
func parseNamespaceObjectYAML(testReq *testRequest, data []byte) error {
var obj map[string]interface{}
var obj map[string]any
if err := yaml.Unmarshal(data, &obj); err != nil {
return fmt.Errorf("failed to unmarshal namespaceObject: %w", err)
}
Expand All @@ -416,7 +416,7 @@ func parseNamespaceObjectYAML(testReq *testRequest, data []byte) error {
// loadGoldFile loads the expected object from a .gold.yaml file.
// parseParamsYAML parses a policy parameters file (ConfigMap or custom resource).
func parseParamsYAML(testReq *testRequest, data []byte) error {
var obj map[string]interface{}
var obj map[string]any
if err := yaml.Unmarshal(data, &obj); err != nil {
return fmt.Errorf("failed to unmarshal params: %w", err)
}
Expand Down Expand Up @@ -475,13 +475,13 @@ func parseAuthorizerYAML(testReq *testRequest, data []byte) error {
return nil
}

// InferOperation determines the Kubernetes admission operation based on which YAML files are present.
// inferOperation determines the Kubernetes admission operation based on which YAML files are present.
// If requestOpStr is non-empty, it's used directly (for explicit CONNECT operations).
// Otherwise, operation is inferred from the presence of object/oldObject files:
// - object only -> CREATE
// - oldObject only -> DELETE
// - both object and oldObject -> UPDATE
func InferOperation(hasObject, hasOldObject bool, requestOpStr string) (string, error) {
func inferOperation(hasObject, hasOldObject bool, requestOpStr string) (string, error) {
// Explicit operation from request.yaml (e.g., CONNECT)
if requestOpStr != "" {
return requestOpStr, nil
Expand Down
Loading
Loading