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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,16 @@ $ go run main.go approvals/kir_test.TestKind.Job.input.yaml | xargs snyk contain
# Docker Scout
$ go run main.go approvals/kir_test.TestKind.Job.input.yaml | xargs docker scout cves
```

## How `kir` treats each document

A manifest stream usually mixes workloads with other objects. `kir` handles each by kind:

| Document | Result |
| --- | --- |
| A workload — `Pod`, `Deployment`, `DaemonSet`, `ReplicaSet`, `StatefulSet`, `Job`, `CronJob` | its images are printed to stdout |
| A valid object with no images — `Service`, `ConfigMap`, `Secret`, … | skipped silently (exit 0) |
| Malformed or unreadable input | reported on stderr, non-zero exit |
| An unrecognized custom resource (CRD) | skipped for now — see [#75](https://github.com/MPV/kir/issues/75) |

So stdout carries only images and stderr stays quiet for normal input. See [ADR 0007](docs/adr/0007-document-classification.md) for the rationale.
1 change: 0 additions & 1 deletion approvals/kir_test.TestError.Service.stderr.approved.txt

This file was deleted.

27 changes: 27 additions & 0 deletions approvals/kir_test.TestMixed.input.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
template:
spec:
containers:
- image: nginx:1.27
---
apiVersion: v1
kind: Service
metadata:
name: web
spec:
ports:
- port: 80
---
apiVersion: batch/v1
kind: Job
metadata:
name: migrate
spec:
template:
spec:
containers:
- image: migrate:v2
2 changes: 2 additions & 0 deletions approvals/kir_test.TestMixed.stdout.approved.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
nginx:1.27
migrate:v2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0
Empty file.
Empty file.
11 changes: 9 additions & 2 deletions approvals/kir_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,23 @@ func TestKind(t *testing.T) {
}
}

func TestError(t *testing.T) {
// A non-workload kind (Service) is skipped: no images, no error, exit 0.
func TestSkipsNonWorkloads(t *testing.T) {
t.Run("Service", func(t *testing.T) {
verify(t, []string{"kir_test.TestError.Service.input.yaml"}, nil)
verify(t, []string{"kir_test.TestSkipsNonWorkloads.Service.input.yaml"}, nil)
})
}

func TestMultiple(t *testing.T) {
verify(t, []string{"kir_test.TestMultiple.input.yaml"}, nil)
}

// A file mixing supported workloads with a non-workload document yields the
// workloads' images; the non-workload is skipped without discarding the rest.
func TestMixed(t *testing.T) {
verify(t, []string{"kir_test.TestMixed.input.yaml"}, nil)
}

// TestCLI covers behaviour that only exists at the CLI boundary — stdin wiring,
// argument resolution, and no-args usage — which no file-argument scenario
// above reaches.
Expand Down
26 changes: 26 additions & 0 deletions docs/adr/0007-document-classification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# 7. How kir classifies each document

- Status: accepted
- Date: 2026-08-08

A manifest stream mixes workloads, image-less objects, custom resources, and the
occasional malformed document. Each falls into one of three tiers:

| Tier | Examples | stdout | stderr | exit |
|---|---|---|---|---|
| Workload (has a PodSpec) | Pod, Deployment, …, CronJob | images | — | 0 |
| Known, image-less | Service, ConfigMap, Secret, … | — | — | 0 |
| Unprocessable | malformed YAML, unreadable file | — | `error: …` | non-zero (#55) |

The load-bearing choice: a valid image-less document is **not** an error and
**not** a warning — it's expected input with nothing to report, so it's skipped
silently. Only unprocessable input hits stderr and the exit code. That keeps
stdout to images, keeps stderr quiet for normal input, and keeps the exit code
trustworthy (an earlier version erred on every Service, which would make
`kir manifests/*` exit non-zero under #55).

Unregistered kinds (CRDs) are a fourth case, today handled like image-less —
skipped silently. But a CRD may embed a PodSpec (Argo Rollouts, Knative, …), so
skipping it silently can drop images (the #49 failure mode). Planned (#75): a
`warning:` on stderr, exit 0 — "seen but not detected" — distinct from the silent
known-image-less tier. Updated when that lands.
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ date the decision was actually made.
| [0004](0004-approval-testing.md) | Pin behaviour with golden (approval) tests | 2025-03-18 |
| [0005](0005-run-entry-point-seam.md) | Expose the CLI as an in-process `Run(args, stdin, stdout, stderr) int` | 2026-08-02 |
| [0006](0006-conventional-commits-and-releases.md) | Automate releases from Conventional Commits | 2026-08-06 |
| [0007](0007-document-classification.md) | How kir classifies each document (workload / image-less / unprocessable) | 2026-08-08 |
20 changes: 18 additions & 2 deletions yamlparser/yamlparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/mpv/kir/k8s"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/client-go/kubernetes/scheme"
)
Expand All @@ -18,6 +19,15 @@ func ProcessData(data []byte) ([]string, error) {
decode := serializer.NewCodecFactory(scheme.Scheme).UniversalDeserializer().Decode
obj, gvk, err := decode(data, nil, nil)
if err != nil {
// Kinds that aren't registered in the scheme (CRDs and other custom
// resources) are skipped rather than failing the whole stream. Some of
// them may embed a PodSpec we could inspect; surfacing those ("seen but
// not detected") is tracked in #75. For now they are skipped silently,
// like any other non-workload document — see
// docs/adr/0007-document-classification.md.
if runtime.IsNotRegisteredError(err) {
return nil, nil
}
return nil, err
}

Expand Down Expand Up @@ -49,7 +59,11 @@ func ProcessData(data []byte) ([]string, error) {
return images, nil
}

return nil, fmt.Errorf("unsupported kind %s", gvk.Kind)
// Any other kind (Service, ConfigMap, ...) is a valid object with no images
// to report, not an error; skip it silently so a single non-workload
// document does not discard images from the rest of the stream. See
// docs/adr/0007-document-classification.md.
return nil, nil
}

func processUnstructured(item unstructured.Unstructured) ([]string, error) {
Expand All @@ -65,5 +79,7 @@ func processUnstructured(item unstructured.Unstructured) ([]string, error) {
}
return images, nil
}
return nil, fmt.Errorf("error: unsupported kind %s in List", gvk.Kind)
// Non-workload items inside a List are skipped, mirroring how top-level
// non-workload documents are handled.
return nil, nil
}
Loading