Day 73 — Elective: k8s client-go or deepen
Day 73 — Elective: k8s client-go or deepen
Stage VII · ~3h
Goal: Choose one track and finish a concrete deliverable—either a minimal Kubernetes client program or a deliberate depth upgrade to your Stage VI service—without starting a second project.
An elective forces decision hygiene: pick, time-box, ship, document. Half of both tracks is a fail. One complete track is a win.
Why this day exists
Stage VII ends soon. Real careers diverge:
- Platform / control-plane engineers touch client-go
- Product engineers deepen domain services (reliability, APIs, data)
Capstone options map roughly:
| Capstone | Elective lean |
|---|---|
| A agent | client-go or deepen observability |
| B microservice | deepen service |
| C CLI | deepen CLI packaging / k8s plugin stretch |
Tomorrow’s Stage VII gate expects evidence of ship quality—not a half-built operator framework.
Decision (do this first — 10 minutes)
Write in ELECTIVE.md:
## Choice
Track: A-k8s | B-deepen
Why (3 bullets):
Out of scope today:
Success criterion:
Cluster/tooling available? (kind/minikube/none):If undecided: B-deepen is the safer default for Stage VIII architecture days.
Decision timer
| Clock | Action |
|---|---|
| 0–10 min | Choose and write ELECTIVE.md |
| If still stuck | Default B |
| If Track A setup >45 min | Switch to B; note why |
Track A — Kubernetes client-go (light)
Theory A1 — What client-go is
k8s.io/client-go is the official Go client for the Kubernetes API:
- Typed clients for core resources (Pods, Deployments, …)
- Informers/listers for efficient watches (advanced)
- Works with kubeconfig (local) or in-cluster config
You will not build an operator framework today. Goal: list/get something real and print a useful summary.
Your program → client-go → kube-apiserver → etcd (cluster)
↑
kubeconfig or in-cluster SA token
Theory A2 — Config resolution
import (
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
)
func newClient() (*kubernetes.Clientset, error) {
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
configOverrides := &clientcmd.ConfigOverrides{}
kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
cfg, err := kubeConfig.ClientConfig()
if err != nil {
return nil, err
}
return kubernetes.NewForConfig(cfg)
}In-cluster (when the binary runs as a Pod):
import "k8s.io/client-go/rest"
cfg, err := rest.InClusterConfig()
if err != nil {
return nil, err
}
return kubernetes.NewForConfig(cfg)Prefer kubeconfig for the lab laptop; mention in-cluster in notes only.
Theory A3 — List pods example
ctx := context.Background()
cs, err := newClient()
if err != nil {
log.Fatal(err)
}
ns := flagNamespace // from --namespace
pods, err := cs.CoreV1().Pods(ns).List(ctx, metav1.ListOptions{
LabelSelector: labelSelector, // optional
})
if err != nil {
log.Fatal(err)
}
for _, p := range pods.Items {
fmt.Printf("%s\t%s\t%s\n", p.Namespace, p.Name, p.Status.Phase)
}Modules are large—use a recent compatible set and follow upstream versioning notes for your cluster version. Pin versions in go.mod and commit go.sum.
Track A lab output
- Program using kubeconfig that lists pods (or deployments) in a namespace.
- Flags:
--namespace, optional label selector.
- Clear error if no cluster (
ELECTIVE.mdcan include “ran against kind/minikube”).
- No full operator / CRD work unless already expert and still time-boxed.
- At least one unit test with fake clientset (even if you never reach a real cluster).
# example local cluster
# kind create cluster
go run ./cmd/klist --namespace defaultTrack A — fake clientset unit test (no cluster)
import (
"context"
"testing"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)
func TestListPods(t *testing.T) {
cs := fake.NewSimpleClientset(&corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "p1", Namespace: "default"},
})
pods, err := cs.CoreV1().Pods("default").List(context.Background(), metav1.ListOptions{})
if err != nil {
t.Fatal(err)
}
if len(pods.Items) != 1 || pods.Items[0].Name != "p1" {
t.Fatalf("got %#v", pods.Items)
}
}Useful when kind install is blocked; still practice client-go types.
RBAC awareness (do not ignore)
Listing pods requires permission. On managed clusters you may lack access—use kind local admin, or stick to fake clientset + documented limitation in ELECTIVE.md.
Track B — Deepen the service
Theory B2 — Quality bar for “deepen”
Deepen means merged improvement with tests, not refactors for their own sake.
Checklist:
- User-visible or operator-visible benefit
- Test or manual script proving it
- NOTES linking to Stage VII tools used
- No new microservice spawned
Example deepen: timeouts everywhere
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
row := s.db.QueryRowContext(ctx, "SELECT id, name FROM items WHERE id = ?", id)Example deepen: idempotency key on create
Document header Idempotency-Key behavior; store key for 24h; return same resource on replay.
key := r.Header.Get("Idempotency-Key")
if key != "" {
if cached, ok := s.idem.Get(key); ok {
writeJSON(w, http.StatusOK, cached)
return
}
}
// create… then s.idem.Put(key, result)Example deepen: retries with jitter
func withRetry(ctx context.Context, attempts int, fn func(context.Context) error) error {
var err error
for i := 0; i < attempts; i++ {
if err = fn(ctx); err == nil {
return nil
}
// simple backoff; prefer full jitter in real systems
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(time.Duration(50*(i+1)) * time.Millisecond):
}
}
return err
}Only retry idempotent operations (or those with idempotency keys).
Worked example — deepen timeouts (Track B)
// before: unbounded
resp, err := http.Get(url)
// after
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
resp, err := client.Do(req)Document in ELECTIVE.md the before/after failure mode (hung handler).
Worked example — Track A CLI shape
func main() {
ns := flag.String("namespace", "default", "Kubernetes namespace")
sel := flag.String("selector", "", "label selector")
flag.Parse()
cs, err := newClient()
if err != nil {
fmt.Fprintf(os.Stderr, "kubeconfig: %v\n", err)
os.Exit(2)
}
if err := listPods(context.Background(), cs, *ns, *sel); err != nil {
fmt.Fprintf(os.Stderr, "list: %v\n", err)
os.Exit(1)
}
}Hour-by-hour suggestion
| Time | Focus |
|---|---|
| 0:00–0:10 | Write ELECTIVE.md choice |
| 0:10–2:10 | Execute one track |
| 2:10–2:40 | Tests/demo commands |
| 2:40–3:00 | Notes + commit readiness for gate |
Common gotchas
| Gotcha | Fix |
|---|---|
| Both tracks half-done | Stop; finish one |
| client-go version hell | Match a tutorial set of module versions |
| Deepen as endless rewrite | Cap scope in ELECTIVE.md first |
| Needing production cluster | kind/k3d/minikube or fake clientset |
| Touching everything before gate | Gate is tomorrow—prefer stability |
| New module for elective | Prefer improving existing service unless A needs isolation |
| Operator envy | List/get only; controllers are multi-week |
| Deepen without proof | Script or test must show delta |
Checkpoint
ELECTIVE.mdrecords choice + why
- One track deliverable runs
- Tests or demo commands documented
- Explicit non-goals listed
- Ready for Stage VII gate evidence pack
- Did not start capstone early
Commit
git add .
git commit -m "day73: elective k8s or deepen"Write three personal gotchas before continuing.
Tomorrow
Day 74 — Stage VII gate: assemble the release-candidate checklist—profiled, scanned, containerized, lint-clean.