Thank you for taking the time to contribute. This document explains how to set up your development environment, add new features, and submit changes.
- Go 1.21+
- A Kubernetes cluster (local via kind or minikube, or any remote cluster)
kubectlconfigured with a working kubeconfig
Click the Fork button on github.com/mdryaan/kubewatch-cli to create your own copy under your GitHub account.
git clone https://github.com/Your-username/kubewatch-cli.git
cd kubewatch-cligit remote add upstream https://github.com/mdryaan/kubewatch-cli.gitgo mod download
make build
./kubewatch versiongit checkout -b feat/your-feature-namekind create cluster --name kubewatch-dev
export KUBECONFIG=$(kind get kubeconfig-path --name kubewatch-dev)
./kubewatch healthmake vet
make buildgit fetch upstream
git rebase upstream/main- Create a new file in
cmd/, e.g.cmd/mycommand.go - Define a
*cobra.Commandvariable and implement theRunEfunction - Register the command in
cmd/root.goinsideinit()viarootCmd.AddCommand(myCmd) - Add any packages your command needs under
pkg/
Example skeleton:
package cmd
import (
"github.com/spf13/cobra"
)
var myCmd = &cobra.Command{
Use: "mycommand",
Short: "Short description of what this command does",
RunE: runMyCommand,
}
func runMyCommand(cmd *cobra.Command, args []string) error {
kc, err := client.New(kubeconfig())
if err != nil {
return err
}
// ... use kc to talk to the cluster
return nil
}- Create a new file in
pkg/watcher/, e.g.pkg/watcher/statefulset_watcher.go - Define a struct embedding
BaseWatcher - Implement the
Watch(ctx, namespace, labelSelector, events chan<- WatchEvent) errormethod using the appropriate clientset lister and watcher - Add a constructor
NewXxxWatcher(kc *client.KubeClient) *XxxWatcher - Wire it up in the relevant
cmd/watch.gosubcommand
The event loop pattern is consistent across all watchers:
func (w *MyWatcher) Watch(ctx context.Context, namespace string, labelSelector string, events chan<- WatchEvent) error {
watcher, err := w.client.Clientset.XxxV1().Resources(namespace).Watch(ctx, metav1.ListOptions{
LabelSelector: labelSelector,
})
if err != nil {
return err
}
defer watcher.Stop()
for {
select {
case <-ctx.Done():
return nil
case event, ok := <-watcher.ResultChan():
if !ok {
return nil
}
// convert and send to events channel
}
}
}- Keep PRs focused on a single concern — one feature, one fix, one refactor
- Title format:
feat(scope): description,fix(scope): description,chore: description - Rebase on
upstream/mainbefore opening a PR; do not merge-commit - All code must compile:
make buildmust succeed make vetmust pass with zero errors- If you add a new package, add a short note to this file explaining where it lives and what it does
- No comments, no docstrings, no inline explanations — well-named identifiers are the documentation
- Use strong Go types everywhere; avoid
interface{}oranyunless forced by an external API - Error values must be wrapped with context using
fmt.Errorf("context: %w", err) - Prefer early returns over nested
ifblocks - Keep function bodies short — if a function exceeds ~50 lines, consider splitting it
- Do not use
init()functions outside ofcmd/root.go - All public types and functions in
pkg/must have meaningful names that explain purpose without comments - Use
context.Contextas the first argument for any function that performs I/O
| Package | Responsibility |
|---|---|
cmd/ |
CLI command definitions only — thin layer over pkg/ |
pkg/client/ |
Kubernetes client construction and configuration |
pkg/watcher/ |
Real-time resource watchers using the Watch API |
pkg/health/ |
Health status assessment logic per resource type |
pkg/anomaly/ |
Anomaly detection rules per failure mode |
pkg/graph/ |
Dependency graph construction and ASCII rendering |
pkg/summary/ |
Namespace-level aggregation and reporting |
pkg/output/ |
All formatting and terminal color helpers |
internal/config/ |
Viper config loading and defaults |
internal/utils/ |
Shared stateless helpers (time, labels, strings) |