Day 73 — Elective: k8s client-go or deepen

Updated

July 30, 2026

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.

Note

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

  1. Program using kubeconfig that lists pods (or deployments) in a namespace.
  2. Flags: --namespace, optional label selector.
  3. Clear error if no cluster (ELECTIVE.md can include “ran against kind/minikube”).
  4. No full operator / CRD work unless already expert and still time-boxed.
  5. 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 default

Track 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 B1 — Depth menu (pick 1–2 only)

Depth Deliverable
Reliability Retries with jitter on outbound calls; timeouts on all I/O
API Pagination + consistent error JSON (Day 53–55 revisit)
Data One more entity + migration; transaction boundary
Observability Finish metrics+traces on the hottest path
Performance Close a Day 65–66 loop in the service, not a toy
Packaging Harden Dockerfile + Makefile release targets

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).


Shared lab rules (both tracks)

Suggested workspace: continue existing module or ~/lab/90daysofx/01-go/day73

  1. Time-box: if Track A cluster setup exceeds 45 minutes, switch to Track B.
  2. One PR/commit story: single coherent change set.
  3. Update ELECTIVE.md with commands and result.
  4. Do not start the capstone early—save integration ambition for Days 82–88.
  5. Leave Stage VII gate inputs stable: container, metrics, govulncheck path still work.

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.md records 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.