Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
e9e0a7e
fix: Phase 0 bug fixes and minor cleanups
abahmed Jun 10, 2026
46af870
test: add Phase 0 unit tests for B1-B7
abahmed Jun 10, 2026
409da1b
refactor: Phase 1 — replace storage.Storage with engine-owned state s…
abahmed Jun 10, 2026
d770c73
refactor: Phase 2 — split pipeline into Detect (pure) + Enrich (I/O)
abahmed Jun 10, 2026
1e02014
refactor: Phase 3 — unify all signals + startup baseline
abahmed Jun 10, 2026
9b7781c
refactor: Phases 4-8 — coverage, lifecycle signals, diagnosis, operab…
abahmed Jun 10, 2026
d33135f
refactor: Remediation — A1-A4 + B-P1 through B-P8 fixes
abahmed Jun 10, 2026
1a088bc
fix: resolve C1-C7 merge-blockers from final review
abahmed Jun 10, 2026
60d61e9
Section 4 regression tests + Section 5.1 DS/SS listers
abahmed Jun 11, 2026
4690fda
feat: events informer backing for pod event lookups
abahmed Jun 11, 2026
e0f76c9
feat: configurable workers knob
abahmed Jun 11, 2026
a1f7fc6
feat: incident-level persisted baseline
abahmed Jun 11, 2026
807466e
feat: PVC thresholds, log-block bound, DS/CronJob monitors, pprof, CR…
abahmed Jun 11, 2026
a95e4a1
fix: baseline healthy-path clear — ClearSeenByPrefix for incident-key…
abahmed Jun 11, 2026
748aac0
chore: remove accidentally committed build binary
abahmed Jun 11, 2026
8905466
fix: gate pprof behind healthCheck.pprof (default off)
abahmed Jun 11, 2026
56cdf96
fix: drop maxLogBlockLines knob, reuse MaxRecentLogLines
abahmed Jun 11, 2026
bf29376
fix: wire restored CRD — crdwatch dynamic informer + live config apply
abahmed Jun 11, 2026
56c6fb7
fix: sort events by timestamp, chart values, DS availability hint
abahmed Jun 11, 2026
a630ce3
feat: crash-frequency escalation (Part B B1)
abahmed Jun 11, 2026
68793c9
feat: HPA-maxed detection (Part B B2)
abahmed Jun 11, 2026
0b93513
feat: TLS certificate expiry detection (Part B B3)
abahmed Jun 11, 2026
f909cfa
feat: add --version flag for operability (Part B B4)
abahmed Jun 11, 2026
9306c12
docs: update README with all new config options (Part B B5)
abahmed Jun 11, 2026
925efdb
fix: shared correlation.ResolveOwnerName, ClearSeenByOwner handler
abahmed Jun 11, 2026
2eaa8a7
fix: rework escalation to crossedTier on UPDATE path
abahmed Jun 11, 2026
d42c08e
fix: HPA sustain window, DesiredReplicas maxed detection
abahmed Jun 11, 2026
2c0e9df
fix: informer-based TLS secret expiry monitor
abahmed Jun 11, 2026
c8effef
feat: node-pod inhibition (Part B4.1)
abahmed Jun 11, 2026
a0fa148
feat: alert storm digest aggregation (Part B4.2)
abahmed Jun 11, 2026
be40d84
feat: fallback provider, GET /incidents, POST /test-alert (Part B4.3)
abahmed Jun 11, 2026
4bb5463
feat: template overrides and renotify (Part B4.4)
abahmed Jun 11, 2026
17f658d
feat: kwatch lint and kwatch replay CLI (Part B4.4)
abahmed Jun 11, 2026
a5e3651
chore: update chart values.schema.json with all config fields
abahmed Jun 11, 2026
355aa96
test: add tests for B4.x features and reworks
abahmed Jun 11, 2026
074eddf
docs: complete README with all new features (Part B5)
abahmed Jun 11, 2026
fa8438e
fix: escalation double-bump, inhibition dead flag, enricher overwrite…
abahmed Jun 11, 2026
3e001a8
docs: reposition README per plan spec (Part B5)
abahmed Jun 11, 2026
253f1d4
chore: add PR template with summary of all phases and verified fixes
abahmed Jun 11, 2026
8d32f94
revert: restore PULL_REQUEST_TEMPLATE.md to original content
abahmed Jun 11, 2026
80f9c46
fix: storm digest delivery and Count (BUG-A)
abahmed Jun 11, 2026
6a29cbc
fix: bugs B-G (CRD severity, replay, race, renotify leak, CRD apply, …
abahmed Jun 11, 2026
706c45b
feat: deviations D1-D6 (per-severity renotify, per-provider templates…
abahmed Jun 11, 2026
b14bc1a
fix: revert baselineDebounce cap, implement write coalescer (D4)
abahmed Jun 11, 2026
3449b16
fix: register klog flags before flag.Parse to prevent crash on -v etc.
abahmed Jun 11, 2026
606dc50
docs: add Upgrading from v0.10.x section with breaking changes
abahmed Jun 11, 2026
1d72f69
fixes
abahmed Jun 11, 2026
9979358
fix: node condition flap — stable incident key per condition type
abahmed Jun 14, 2026
de0a812
test: add tests for node flap, baseline seeding, log leak, high-resta…
abahmed Jun 14, 2026
e200d42
fix: MarkResolved idempotency guard + revert infra baseline seeding
abahmed Jun 14, 2026
5af7252
feat: resolve hold-down (flap dampening) + shared IncidentKey for bas…
abahmed Jun 14, 2026
b869fbb
feat: per-pod baseline + resolved→create store fix + pending-resolve …
abahmed Jun 14, 2026
3185622
feat: backlog A1-A4, B1-B8, §5 residuals
abahmed Jun 14, 2026
be40e7e
fix: braced-only env interpolation, RBAC for namespaceSelector, struc…
abahmed Jun 14, 2026
b1c65db
fix: BUG-I stale node inhibition, BUG-II dead ClearSeen, BUG-III k8s …
abahmed Jun 14, 2026
f9258a0
feat: Rule 1 edge-triggered notification + Rule 2 incident grain (own…
abahmed Jun 14, 2026
8aa4dba
feat: defaults pass + HealthCheck.Diagnostics + chart improvements
abahmed Jun 14, 2026
e50e3a3
fixes
abahmed Jun 14, 2026
b5fa247
feat: §5f renotify collapse, IMP-1 async delivery, IMP-2 self-metrics
abahmed Jun 14, 2026
9e82cc7
chore: §5 test hygiene, IMP-7 message-size, IMP-8 256Mi memory
abahmed Jun 14, 2026
b374f68
feat: IMP-5 PeakResources+runbooks, §5b image-pull normalization
abahmed Jun 14, 2026
e27d815
feat: IMP-3 integration harness, IMP-4 Signal, §5f deprecation logs, …
abahmed Jun 14, 2026
1c2a655
docs: README new defaults/upgrading, IMP-4 all-handler Signal, helm test
abahmed Jun 14, 2026
f027aa2
fix: injectable clock for handler (h.now()) — replaces direct time.No…
abahmed Jun 14, 2026
a964885
fix: container set in update/resolved messages (alert.go + Slack)
abahmed Jun 14, 2026
8699335
chore: test hygiene — replace os.Setenv/defer cleanup with t.Setenv/t…
abahmed Jun 14, 2026
cdfaeef
fix: FIX-1 (P0 crash) — assign controller-level CronJob/HPA listers
abahmed Jun 14, 2026
e352652
FIX-4+FIX-5: rune-safe truncateMsg + per-provider maxBytes
abahmed Jun 14, 2026
336615d
FIX-3: remove flat renotify.interval field
abahmed Jun 14, 2026
c9263b4
FIX-2+NEW-1: async delivery with per-provider channels, backoff, DLQ
abahmed Jun 14, 2026
8ee47c7
NEW-3: stable incident_id in logs + NEW-5: strict YAML lint
abahmed Jun 14, 2026
f05c9ba
copy-on-emit: clone incident before async enqueue (point 7) + NEW-2: …
abahmed Jun 14, 2026
7c8fa66
chore: ActiveCount, containerDisplayName, ProviderNames + provider va…
abahmed Jun 15, 2026
7de55ad
feat: P2 noise reducers, silences consolidation, provider verify, loa…
abahmed Jun 15, 2026
cb13482
fix: Phase 0 bug fixes and hardening
abahmed Jun 15, 2026
72fdf5d
feat: HPA-2 scaling error detection, BASE-1 restart parity, CHRONIC-1…
abahmed Jun 15, 2026
f4a3505
feat: AI incident enrichment + cheap-depth enhancements
abahmed Jun 16, 2026
c3248e1
fix: kwatch-llm non-root + shutdown panic + breaker single-probe + 64…
abahmed Jun 17, 2026
98034b6
fix: apply scan batch + remaining items (digested, GOMEMLIMIT, fanOut…
abahmed Jun 17, 2026
c6e3f78
fix: thread context.Context through filter pipeline, remove context.T…
abahmed Jun 17, 2026
49ebc19
fix: wrap GetPodEvents/GetPVNameFromPVC with timeout + thread ctx thr…
abahmed Jun 17, 2026
d7dcd53
fix: batch 11 fixes — storm digest, pprof auth, ctx threading, metric…
abahmed Jun 19, 2026
243a439
fix: address CodeQL unsafe quoting in feishu — use json.Marshal with …
abahmed Jun 20, 2026
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: 8 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,11 @@ updates:
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
36 changes: 36 additions & 0 deletions .github/workflows/publish-llm.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Publish LLM image

on:
workflow_dispatch:
inputs:
version:
description: "model image version, e.g. v2"
required: true

jobs:
push_llm_image:
name: Push kwatch-llm image to GHCR
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Free disk space
run: docker buildx prune -af && df -h
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: deploy/llm
platforms: linux/amd64,linux/arm64
push: true
build-args: |
MODEL_VERSION=${{ github.event.inputs.version }}
tags: |
ghcr.io/abahmed/kwatch-llm:${{ github.event.inputs.version }}
ghcr.io/abahmed/kwatch-llm:latest
3 changes: 3 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ jobs:
push_to_registry:
name: Push Docker image to GitHub Container Registry
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- name: Check out the repo
uses: actions/checkout@v4
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ config.yaml
*.DS_Store

# debug
__debug_bin*
__debug_bin*
/kwatch
4 changes: 2 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceRoot}",
"program": "${workspaceRoot}/cmd/kwatch",
"showLog": true,
"env": {
"CONFIG_FILE": "config.yaml"
"CONFIG_FILE": "${workspaceRoot}/config.yaml"
}
}
]
Expand Down
121 changes: 121 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Changelog

All notable changes to this project are documented in this file.

## [Unreleased]

### Added

- **AI incident enrichment (opt-in, default-off)**: self-hosted LLM sidecar
(`kwatch-llm` — Qwen2.5-Coder-1.5B-Instruct via Ollama) appends root-cause
analysis to incident alerts. Everything runs in-cluster; no external API
calls. Configuration: `llm.enabled` (default `false`). Circuit breaker
(3-failure / 60s cooldown), single-flight enrich channel, 15s timeout.
- `internal/llm/` — client, prompt builder with CD-1 grounding,
`selectRelevant` log extraction, `redact` (password/token/JWT scrubbing)
- Baked model image at `deploy/llm/` with Makefile target & CI
(`publish-llm.yml`)
- Helm: `config.llm.enabled`, `llm.nativeSidecar`, replica guard
- Metrics: `kwatch_llm_enrich_total`, `kwatch_llm_enrich_failed_total`,
`kwatch_llm_enrich_skipped_total`

- **Cheap-depth enhancements (always-on, no LLM needed)**:
| ID | What |
|----|------|
| CD-1 | Grounding fields in enrichment (occurrences, affected pods, duration) |
| CD-2 | Correlation surfaces affected-pod count + per-node breakdown |
| CD-3 | Built-in signature hints (Postgres OOM, TLS handshake, disk-full, timeout, RBAC, OOM-without-limit) |
| CD-4 | `runbooks:` URL appended to incident hint |
| CD-5 | Investigate commands (`kubectl logs`/`kubectl describe`) + dashboard deep-link |

### Fixed

#### Phase 0 bugs

- **CR-1**: LifecycleHook data race — `MarkResolved`, `RemovePod`,
`ResolveByResource`, and `checkLifecycle` renotify now clone the incident
under lock before passing it to the hook callback.
- **CR-2**: Node inhibition cleared during hold-down — `ResolveByResource` no
longer unconditionally deletes `activeNodeIncidents`; the hold-down pathway
re-inserts the inhibition entry when a resource-level incident remains open.
- **PVC-3**: Swallowed node-level error auto-resolves PVCs — added
`incomplete` flag; `getNodeUsage` errors set `incomplete=true` and the
resolve-by-absence path skips incomplete cycles.
- **BUG-1**: CronJob false positives — split nil `LastScheduleTime` and
staleness branches; new CronJob only alerts if `CreationTimestamp` > 24h ago;
stale check only fires when `LastScheduleTime` is non-nil and > 24h old.
- **BUG-2**: PodStatusFilter "Added" casing — switched to
`strings.EqualFold(ctx.EvType, "Added")`.
- **BUG-3**: OOMKilled hint for containers with no memory limit — added
dedicated hint string when `resources.limits.memory` is nil or zero.
- **BUG-4**: Per-signal `IncludeEvents`/`IncludeLogs` overwritten by
`eventWithConfig` — removed dead `IncludeEvents`/`IncludeLogs` fields from
the `Signal` struct.
- **BUG-5**: `tls_sweep` clock — changed `time.Until(expiry)` to
`expiry.Sub(now)`.
- **BUG-6**: Init container failures now use `InitContainerError` hint — added
`IsInit bool` to `ContainerContext`, set when iterating
`InitContainerStatuses`, and `buildContainerHint` prefers the
`InitContainerError` hint when `IsInit && exitCode != 0`.
- **BUG-7**: Crashloop incidents dropped on transient clean exit — added
`!ctx.Container.HasRestarts &&` guard to the skip condition for
`Terminated{Completed|ExitCode 0|143}`.
- **F2**: Drop-oldest channel send can block — the drain-receive path is now
non-blocking (`select` with `default`).
- **SCAN-1**: `ActiveCount` overcount — now iterates `e.state` counting only
`State != StateResolved` instead of returning `len(e.state)`.
- **SCAN-3**: AlertManager `Init` races with concurrent `NotifyIncident` calls
— added `a.mu` mutex; `Start` snapshots entries; `Notify`, `NotifyEvent`,
`NotifyIncident` snapshot entries; `shutdown` is idempotent.
- **LOG-1**: Container detection log level — changed `klog.InfoS` to
`klog.V(2).InfoS` in `execute_containers_filters.go`.
- **SQ-1**: Remove `startupQuiet` — deleted field from config and engine;
removed wiring in `main.go` and CRD types.
- **PVC-1**: Kubelet proxy calls use request context — threaded
`context.Context` through `PvcMonitor.checkUsage` → `getNodeUsage` →
`GetNodeSummary`/`GetNodes`/`GetPVNameFromPVC`/`GetPodContainerLogs`/`GetPodEvents`;
each wraps the call with `context.WithTimeout(ctx, 10s)`.
- **PVC-2**: N+1 PVC API fan-out — `checkUsage` builds a `pvByPVC` map
(ns/name → spec.VolumeName) once per cycle via one `List` call.
- **PVC-4**: Nil PodRef panic — added `if pod.PodRef == nil { continue }`.
- **PVC-5**: Division by zero — added `if vol.CapacityBytes <= 0 { continue }`.
- **HTTP-1**: Discord/Telegram no timeout — replaced `&http.Client{}` with
`k8s.GetDefaultClient()` (shared client with proper transport and timeout).
- **HTTP-2**: Unbounded log fetch when `maxRecentLogLines == 0` — default tail
of 500 lines and 1 MB `LimitBytes` cap.
- **HB-1**: Heartbeat ping not tied to request context — `ping()` now accepts
`context.Context` and creates the HTTP request with `NewRequestWithContext`.
- **MAIN-1/MAIN-2**: Shutdown/exit — replaced `os.Exit(0)` with a `stop`
channel; lost-leader exits with code 1; graceful shutdown uses a fresh
timeout context.
- **HEALTH-1**: `/readyz` static OK — added `ready atomic.Bool`;
`SetReady(true)` at end of `runLeaderTasks`; returns 503 when not ready.
- **Severity default table**: Added `defaultSeverityByReason` map with
`Evicted → medium`, `ImagePullBackOff → medium`. Looked up in
`resolveSeverity` between user `SeverityByReason` and owner-kind check.
- **Escalation + default severities**: Added `severityForTier(tierIdx, current)`
that computes tier-based severity, preferring the higher of the tier and the
current severity. Prevents double-escalation when a default reason severity
is already set.

### Added

- **NEW-4**: `internal/integration/controller_fault_test.go` — integration test
(build tag `integration`) with fake clientset that verifies the controller
tolerates processing pod events with no providers configured.

### Removed

- `correlation.startupQuiet` config field and CRD field.
- `IncludeEvents`/`IncludeLogs` fields from `event.Signal` struct (dead code).
- `StartupQuiet` field from `config/config.go` and `api/v1alpha1/types.go`.

### Changed

- `kubernetes.Interface` methods (`GetNodes`, `GetPVNameFromPVC`,
`GetNodeSummary`, `GetPodContainerLogs`, `GetPodEvents`) now accept
`context.Context` as the first parameter.
- `discord.Verify()`, `telegram.Verify()`, `telegram.sendByTelegramApi()` now
use `k8s.GetDefaultClient()` instead of `&http.Client{}`.
- `HeartbeatMonitor.ping()` now requires a `context.Context` parameter.
- `PvcMonitor.checkUsage()` now requires a `context.Context` parameter.
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,7 @@ docker-build:
# Docker build with latest tag
docker-build-latest:
docker build -t kwatch:latest -t kwatch:$(VERSION) .

# Build LLM sidecar image
llm-image:
PUSH=$(PUSH) TAG=$(TAG) deploy/llm/build.sh
3 changes: 3 additions & 0 deletions api/v1alpha1/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// +k8s:deepcopy-gen=package

package v1alpha1
111 changes: 111 additions & 0 deletions api/v1alpha1/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package v1alpha1

import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// KwatchConfig is the schema for the kwatch deployment configuration.
type KwatchConfig struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec KwatchConfigSpec `json:"spec"`
}

// KwatchConfigSpec defines the desired kwatch configuration.
type KwatchConfigSpec struct {
MaxRecentLogLines int64 `json:"maxRecentLogLines,omitempty"`
IgnoreFailedGracefulShutdown bool `json:"ignoreFailedGracefulShutdown,omitempty"`
Namespaces []string `json:"namespaces,omitempty"`
Reasons []string `json:"reasons,omitempty"`
IgnoreContainerNames []string `json:"ignoreContainerNames,omitempty"`
IgnorePodNames []string `json:"ignorePodNames,omitempty"`
IgnoreLogPatterns []string `json:"ignoreLogPatterns,omitempty"`
SeverityByOwnerKind map[string]string `json:"severityByOwnerKind,omitempty"`
PendingPodThreshold int `json:"pendingPodThreshold,omitempty"`
ResyncSeconds int `json:"resyncSeconds,omitempty"`
Silences []SilenceRule `json:"silences,omitempty"`
Correlation CorrelationConfig `json:"correlation,omitempty"`
PvcMonitor PvcMonitorConfig `json:"pvcMonitor,omitempty"`
NodeMonitor NodeMonitorConfig `json:"nodeMonitor,omitempty"`
RolloutMonitor RolloutMonitorConfig `json:"rolloutMonitor,omitempty"`
DaemonSetMonitor DaemonSetMonitorConfig `json:"daemonSetMonitor,omitempty"`
JobMonitor JobMonitorConfig `json:"jobMonitor,omitempty"`
CronJobMonitor CronJobMonitorConfig `json:"cronJobMonitor,omitempty"`
HeartbeatMonitor HeartbeatMonitorConfig `json:"heartbeatMonitor,omitempty"`
HealthCheck HealthCheckConfig `json:"healthCheck,omitempty"`
App AppConfig `json:"app,omitempty"`
Workers int `json:"workers,omitempty"`
}

type CorrelationConfig struct {
Window int `json:"window,omitempty"`
Cooldown int `json:"cooldown,omitempty"`
StaleThreshold int `json:"staleThreshold,omitempty"`
LifecycleInterval int `json:"lifecycleInterval,omitempty"`
}

type PvcMonitorConfig struct {
Enabled bool `json:"enabled,omitempty"`
Interval int `json:"interval,omitempty"`
Threshold float64 `json:"threshold,omitempty"`
CriticalThreshold float64 `json:"criticalThreshold,omitempty"`
}

type NodeMonitorConfig struct {
Enabled bool `json:"enabled,omitempty"`
}

type RolloutMonitorConfig struct {
Enabled bool `json:"enabled,omitempty"`
}

type DaemonSetMonitorConfig struct {
Enabled bool `json:"enabled,omitempty"`
}

type JobMonitorConfig struct {
Enabled bool `json:"enabled,omitempty"`
}

type CronJobMonitorConfig struct {
Enabled bool `json:"enabled,omitempty"`
}

type HeartbeatMonitorConfig struct {
Enabled bool `json:"enabled,omitempty"`
Interval int `json:"interval,omitempty"`
URL string `json:"url,omitempty"`
}

type HealthCheckConfig struct {
Enabled bool `json:"enabled,omitempty"`
Port int `json:"port,omitempty"`
}

type AppConfig struct {
ClusterName string `json:"clusterName,omitempty"`
ProxyURL string `json:"proxyURL,omitempty"`
DisableStartupMessage bool `json:"disableStartupMessage,omitempty"`
LogFormatter string `json:"logFormatter,omitempty"`
}

type SilenceRule struct {
Namespaces []string `json:"namespaces,omitempty"`
Reasons []string `json:"reasons,omitempty"`
PodNamePatterns []string `json:"podNamePatterns,omitempty"`
ContainerNames []string `json:"containerNames,omitempty"`
LogPatterns []string `json:"logPatterns,omitempty"`
ContainerMessages []string `json:"containerMessages,omitempty"`
NodeReasons []string `json:"nodeReasons,omitempty"`
NodeMessages []string `json:"nodeMessages,omitempty"`
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// KwatchConfigList contains a list of KwatchConfig.
type KwatchConfigList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []KwatchConfig `json:"items"`
}
Loading
Loading