Skip to content

Commit 237a093

Browse files
author
Abhaya Pattanaik
committed
Optimize ReplicaSet calls, add cross-platform binaries, terse output fixes
1 parent c06ea61 commit 237a093

6 files changed

Lines changed: 100 additions & 31 deletions

File tree

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ When an alert fires, one command should tell you everything: what's failing (fro
44

55
80% of Kubernetes outages trace to recent changes ([Komodor, 2025](https://komodor.com/blog/komodor-2025-enterprise-kubernetes-report-finds-nearly-80-of-production-outages/)). Yet today, finding them is a slog. Kubectl events expire in an hour. ArgoCD shows syncs, not details. Slack becomes your search engine. Forty-five minutes later: someone found a ConfigMap edit.
66

7-
Our first release fills a gap: a unified change timeline across every resource type—images, ConfigMaps, Secrets, RBAC, HPA, NetworkPolicy, env vars, node events—in a single command. No open-source CLI has this today.
7+
Our first release fills a gap: a unified change timeline across common Kubernetes resource types—images, ConfigMaps, Secrets, RBAC, HPA, NetworkPolicy, env vars, node events—in a single command. No open-source CLI has this today.
88

99
<!-- TODO: demo GIF -->
1010
<!-- ![kblame demo](docs/demo.gif) -->
@@ -29,7 +29,7 @@ Here's how we're building toward the full vision:
2929

3030
| Version | Functionality | Status |
3131
|---------|---------|--------|
32-
| **V1** | What changed — unified change timeline across all resource types | Current |
32+
| **V1** | What changed — unified change timeline across common Kubernetes resource types | Current |
3333
| **V2** | What changed + what's failing — logs, confirmed alert correlation | Planned |
3434
| **V3** | What changed + what's failing + how they connect — dependency-aware correlation, persistent history via in-cluster controller | Planned |
3535
| **V4** | Multi-cluster, web UI, learned patterns | Planned |
@@ -62,11 +62,11 @@ kblame is different:
6262

6363
| Platform | Architecture | Download | Status |
6464
|----------|--------------|----------|--------|
65-
| macOS | ARM64 (Apple Silicon) | [kblame-darwin-arm64](https://github.com/abcdedf/kblame/releases) | Available |
66-
| macOS | AMD64 (Intel) | Coming soon | Planned |
67-
| Linux | AMD64 | Coming soon | Planned |
68-
| Linux | ARM64 | Coming soon | Planned |
69-
| Windows | AMD64 | Coming soon | Planned |
65+
| macOS | ARM64 (Apple Silicon) | [kblame-darwin-arm64](https://github.com/abcdedf/kblame/releases/download/latest/kblame-darwin-arm64) | Available |
66+
| macOS | AMD64 (Intel) | [kblame-darwin-amd64](https://github.com/abcdedf/kblame/releases/download/latest/kblame-darwin-amd64) | Available |
67+
| Linux | AMD64 | [kblame-linux-amd64](https://github.com/abcdedf/kblame/releases/download/latest/kblame-linux-amd64) | Available |
68+
| Linux | ARM64 | [kblame-linux-arm64](https://github.com/abcdedf/kblame/releases/download/latest/kblame-linux-arm64) | Available |
69+
| Windows | AMD64 | [kblame-windows-amd64.exe](https://github.com/abcdedf/kblame/releases/download/latest/kblame-windows-amd64.exe) | Available |
7070

7171
**From source** (requires Go 1.22+):
7272
```bash

pkg/changes/collector.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"sync"
88
"time"
99

10+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11+
1012
"github.com/abcdedf/kblame/internal/k8s"
1113
)
1214

@@ -42,6 +44,27 @@ func (c *Collector) SetDetectorTimeout(d time.Duration) {
4244
func (c *Collector) Collect(ctx context.Context, namespace string, sinceTime time.Time) ([]Change, error) {
4345
dets := RegisteredDetectors()
4446

47+
// Pre-fetch ReplicaSet data once for all RS-aware detectors, reducing
48+
// redundant LIST calls from N to 1.
49+
var rsAware []ReplicaSetAwareDetector
50+
for _, d := range dets {
51+
if rsd, ok := d.(ReplicaSetAwareDetector); ok {
52+
rsAware = append(rsAware, rsd)
53+
}
54+
}
55+
if len(rsAware) > 0 {
56+
rsList, err := c.client.ListReplicaSets(ctx, namespace, metav1.ListOptions{})
57+
if err != nil {
58+
fmt.Fprintf(ioStderr, "warning: failed to pre-fetch replicasets: %v\n", err)
59+
// Detectors will fall back to fetching individually.
60+
} else {
61+
deployRS := groupReplicaSetsByDeployment(rsList)
62+
for _, rsd := range rsAware {
63+
rsd.SetReplicaSetData(deployRS)
64+
}
65+
}
66+
}
67+
4568
type result struct {
4669
changes []Change
4770
err error
@@ -94,6 +117,11 @@ func (c *Collector) Collect(ctx context.Context, namespace string, sinceTime tim
94117
}
95118
}
96119

120+
// Clear pre-fetched data so detectors don't hold stale state.
121+
for _, rsd := range rsAware {
122+
rsd.SetReplicaSetData(nil)
123+
}
124+
97125
// Sort by timestamp, newest first.
98126
sort.Slice(allChanges, func(i, j int) bool {
99127
return allChanges[i].Timestamp.After(allChanges[j].Timestamp)

pkg/changes/detector.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,20 @@ type Detector interface {
2525
Detect(ctx context.Context, client *k8s.Client, namespace string, since time.Time) ([]Change, error)
2626
}
2727

28+
// ReplicaSetAwareDetector is an optional interface that detectors may implement
29+
// to receive pre-fetched ReplicaSet data. When the Collector detects that
30+
// multiple detectors need ReplicaSet data, it fetches the list once and injects
31+
// it into all implementors before calling Detect. This reduces redundant LIST
32+
// calls from N to 1.
33+
type ReplicaSetAwareDetector interface {
34+
Detector
35+
// SetReplicaSetData provides pre-fetched, grouped ReplicaSet data.
36+
// The collector calls this before Detect. Implementations should store
37+
// the data and use it instead of calling ListReplicaSets themselves.
38+
// Passing nil clears any cached data.
39+
SetReplicaSetData(data map[string][]rsInfo)
40+
}
41+
2842
// registry holds all registered detectors. Access is guarded by mu.
2943
var (
3044
mu sync.RWMutex

pkg/changes/detector_envvar.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,19 @@ import (
99
"github.com/abcdedf/kblame/internal/k8s"
1010
)
1111

12-
// Compile-time interface check.
12+
// Compile-time interface checks.
1313
var _ Detector = (*envvarDetector)(nil)
14+
var _ ReplicaSetAwareDetector = (*envvarDetector)(nil)
1415

1516
// envvarDetector detects environment variable changes by comparing consecutive
1617
// ReplicaSet revisions owned by the same Deployment.
17-
type envvarDetector struct{}
18+
type envvarDetector struct {
19+
prefetchedRS map[string][]rsInfo
20+
}
21+
22+
func (d *envvarDetector) SetReplicaSetData(data map[string][]rsInfo) {
23+
d.prefetchedRS = data
24+
}
1825

1926
func init() {
2027
Register(&envvarDetector{})
@@ -27,14 +34,16 @@ func (d *envvarDetector) Description() string {
2734
}
2835

2936
func (d *envvarDetector) Detect(ctx context.Context, client *k8s.Client, namespace string, since time.Time) ([]Change, error) {
30-
rsList, err := client.ListReplicaSets(ctx, namespace, metav1.ListOptions{})
31-
if err != nil {
32-
warnf("%s: failed to list replicasets: %v", d.Name(), err)
33-
return nil, nil
37+
deployRS := d.prefetchedRS
38+
if deployRS == nil {
39+
rsList, err := client.ListReplicaSets(ctx, namespace, metav1.ListOptions{})
40+
if err != nil {
41+
warnf("%s: failed to list replicasets: %v", d.Name(), err)
42+
return nil, nil
43+
}
44+
deployRS = groupReplicaSetsByDeployment(rsList)
3445
}
3546

36-
deployRS := groupReplicaSetsByDeployment(rsList)
37-
3847
var changes []Change
3948
for _, rss := range deployRS {
4049
if len(rss) < 2 {

pkg/changes/detector_image.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,19 @@ import (
1111
"github.com/abcdedf/kblame/internal/k8s"
1212
)
1313

14-
// Compile-time interface check.
14+
// Compile-time interface checks.
1515
var _ Detector = (*imageDetector)(nil)
16+
var _ ReplicaSetAwareDetector = (*imageDetector)(nil)
1617

1718
// imageDetector detects container image changes by comparing consecutive
1819
// ReplicaSet revisions owned by the same Deployment.
19-
type imageDetector struct{}
20+
type imageDetector struct {
21+
prefetchedRS map[string][]rsInfo
22+
}
23+
24+
func (d *imageDetector) SetReplicaSetData(data map[string][]rsInfo) {
25+
d.prefetchedRS = data
26+
}
2027

2128
func init() {
2229
Register(&imageDetector{})
@@ -29,14 +36,16 @@ func (d *imageDetector) Description() string {
2936
}
3037

3138
func (d *imageDetector) Detect(ctx context.Context, client *k8s.Client, namespace string, since time.Time) ([]Change, error) {
32-
rsList, err := client.ListReplicaSets(ctx, namespace, metav1.ListOptions{})
33-
if err != nil {
34-
warnf("%s: failed to list replicasets: %v", d.Name(), err)
35-
return nil, nil
39+
deployRS := d.prefetchedRS
40+
if deployRS == nil {
41+
rsList, err := client.ListReplicaSets(ctx, namespace, metav1.ListOptions{})
42+
if err != nil {
43+
warnf("%s: failed to list replicasets: %v", d.Name(), err)
44+
return nil, nil
45+
}
46+
deployRS = groupReplicaSetsByDeployment(rsList)
3647
}
3748

38-
deployRS := groupReplicaSetsByDeployment(rsList)
39-
4049
var changes []Change
4150
for _, rss := range deployRS {
4251
if len(rss) < 2 {

pkg/changes/detector_resource.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,19 @@ import (
1212
"github.com/abcdedf/kblame/internal/k8s"
1313
)
1414

15-
// Compile-time interface check.
15+
// Compile-time interface checks.
1616
var _ Detector = (*resourceDetector)(nil)
17+
var _ ReplicaSetAwareDetector = (*resourceDetector)(nil)
1718

1819
// resourceDetector detects resource request/limit changes (CPU, memory) by
1920
// comparing consecutive ReplicaSet revisions owned by the same Deployment.
20-
type resourceDetector struct{}
21+
type resourceDetector struct {
22+
prefetchedRS map[string][]rsInfo
23+
}
24+
25+
func (d *resourceDetector) SetReplicaSetData(data map[string][]rsInfo) {
26+
d.prefetchedRS = data
27+
}
2128

2229
func init() {
2330
Register(&resourceDetector{})
@@ -30,14 +37,16 @@ func (d *resourceDetector) Description() string {
3037
}
3138

3239
func (d *resourceDetector) Detect(ctx context.Context, client *k8s.Client, namespace string, since time.Time) ([]Change, error) {
33-
rsList, err := client.ListReplicaSets(ctx, namespace, metav1.ListOptions{})
34-
if err != nil {
35-
warnf("%s: failed to list replicasets: %v", d.Name(), err)
36-
return nil, nil
40+
deployRS := d.prefetchedRS
41+
if deployRS == nil {
42+
rsList, err := client.ListReplicaSets(ctx, namespace, metav1.ListOptions{})
43+
if err != nil {
44+
warnf("%s: failed to list replicasets: %v", d.Name(), err)
45+
return nil, nil
46+
}
47+
deployRS = groupReplicaSetsByDeployment(rsList)
3748
}
3849

39-
deployRS := groupReplicaSetsByDeployment(rsList)
40-
4150
var changes []Change
4251
for _, rss := range deployRS {
4352
if len(rss) < 2 {

0 commit comments

Comments
 (0)