Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
b4e5267
2026-05-19: GKE phase 2c — design spec + implementation plan (edit fl…
slayer May 19, 2026
eaf4081
2026-05-19: GKE phase 2c — edit projections + request builders
slayer May 19, 2026
92bb83e
2026-05-19: GKE phase 2c — guard buildSetResourceLabelsRequest agains…
slayer May 19, 2026
9a9a0b1
2026-05-19: GKE phase 2c — UpdateClusterBasic + SetMaintenancePolicy …
slayer May 19, 2026
3bbd5c2
2026-05-19: GKE phase 2c — consolidate edit sentinels + add dispatch …
slayer May 19, 2026
b1a063c
2026-05-19: GKE phase 2c — extend ClusterDetails + NodePool projectio…
slayer May 19, 2026
6148559
2026-05-19: GKE phase 2c — edit message types (open/request/cancel/re…
slayer May 19, 2026
8556cb1
2026-05-19: GKE phase 2c — cluster edit form view (3-state machine + …
slayer May 19, 2026
9cf4d5b
2026-05-19: GKE phase 2c — cluster edit: delegate non-key messages so…
slayer May 19, 2026
5f66eb5
2026-05-19: GKE phase 2c — node pool edit form view (3-state machine …
slayer May 19, 2026
9f659f0
2026-05-19: GKE phase 2c — pool edit: guard unknown strategy against …
slayer May 19, 2026
a789498
2026-05-19: GKE phase 2c — wire e key for cluster + pool edit
slayer May 19, 2026
ec875bc
2026-05-19: GKE phase 2c — register edit views + handlers + sequentia…
slayer May 19, 2026
10e97ea
2026-05-19: GKE phase 2c — docs (CLAUDE.md, README.md, key-bindings)
slayer May 19, 2026
f7db0bb
2026-05-19: GKE phase 2c — review fixes (Copilot round 1)
slayer May 19, 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
2 changes: 2 additions & 0 deletions .claude/rules/key-bindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,7 @@ arrow does the same without needing to select the row first.
| Key | Action |
|-----|--------|
| `u` | Upgrade control plane (version picker; hidden on managed release channels) |
| `e` | Edit cluster — logging/monitoring services + daily maintenance window (diff preview before deploy) |

### GKE Cluster Details — Node Pools tab actions (Standard clusters only)

Expand All @@ -462,6 +463,7 @@ arrow does the same without needing to select the row first.
| `D` | Delete focused pool (type-to-confirm) |
| `R` | Resize focused pool (manual or autoscale) |
| `u` | Upgrade focused pool (version picker) |
| `e` | Edit focused pool — auto-upgrade/auto-repair toggles + upgrade strategy/max-surge/max-unavailable (diff preview) |

## SQL Instances View

Expand Down
7 changes: 6 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ On developing or updating a new feature keep in mind the following guidelines:
- Network LBs (passthrough / proxy / legacy) render an explicit
placeholder on the Observability tab — `l3/*` metric family is on
the roadmap.
- [x] GKE cluster management (Phase 1 + 2a + 2b)
- [x] GKE cluster management (Phase 1 + 2a + 2b + 2c)
- Clusters list across all locations (zonal + regional, Autopilot + Standard)
- Cluster details with Overview, Node Pools, Nodes, Observability, and Logs tabs
- Type-to-confirm cluster delete with explicit warning about non-auto-deleted
Expand All @@ -372,6 +372,11 @@ On developing or updating a new feature keep in mind the following guidelines:
- Upgrade control plane and individual node pools (version picker)
- Long-running operations tracked in footer with 5 s polling; refresh on DONE
- Autopilot clusters hide pool actions; release-channel clusters hide master upgrade
- Phase 2c edit flows (`e` key):
- Cluster edit (Overview tab, both Standard + Autopilot): logging/monitoring services + daily maintenance window with diff preview before deploy
- Node pool edit (Node Pools tab, Standard only): auto-upgrade/auto-repair toggles + upgrade strategy/max-surge/max-unavailable
- Multi-endpoint edits (e.g. services + maintenance) run sequentially under one `gke-op:` footer task; status updates as each step lands
- Unknown upgrade strategies (legacy `SHORT_LIVED`, etc.) surface as a placeholder without flagging a false-positive diff

## Planned Features
- [x] Subnets list and management
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ Without mise, install Go 1.26+ and `golangci-lint` 2.12+ manually.
<details>
<summary><strong>Kubernetes Engine</strong></summary>

- **GKE (Kubernetes Engine)**: clusters list (Autopilot + Standard, zonal + regional), 5-tab cluster details (Overview, Node Pools, Nodes, Observability, Logs), type-to-confirm delete with non-auto-deleted resource warning, cluster-scope monitoring charts with time range + auto-refresh, embedded logs viewer with severity toggles. Phase 2b adds node-pool create/delete/resize and control-plane/node-pool upgrades with footer-tracked long-running operations.
- **GKE (Kubernetes Engine)**: clusters list (Autopilot + Standard, zonal + regional), 5-tab cluster details (Overview, Node Pools, Nodes, Observability, Logs), type-to-confirm delete with non-auto-deleted resource warning, cluster-scope monitoring charts with time range + auto-refresh, embedded logs viewer with severity toggles. Phase 2b adds node-pool create/delete/resize and control-plane/node-pool upgrades with footer-tracked long-running operations. Phase 2c adds cluster + node-pool edit flows (`e` key) with diff preview — logging/monitoring + daily maintenance for clusters, management toggles + upgrade settings for pools.
</details>

<details>
Expand Down
284 changes: 284 additions & 0 deletions doc/2026-05-19-gke-phase2c-edit/Design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
# GKE Phase 2c — cluster + node pool edit

## Goal

Add `e` key edit flows for existing GKE clusters and node pools — the
fields most commonly tweaked post-create, with a diff preview before
deploy. Like Phase 2b mutations, every edit kicks a long-running
`Operation`; the footer polls it via the existing `pollGKEOperation`
helper until DONE.

## Non-goals

- **Immutable fields** — name, location, network, subnetwork, IP family,
initial pod range, autopilot flag (cluster); machine type, disk
type/size, preemptible flag (pool). The form does not surface these.
- **Already-shipped mutations** — node count (Phase 2b
`SetNodePoolSize`), autoscale (`UpdateNodePoolAutoscaling`),
version upgrades (Phase 2b). The edit form does NOT duplicate them
to keep field overlap minimal.
- **Recurring maintenance windows + exclusions** — only daily window for
MVP. RRULE-based recurrence and exclusion intervals are deferred.
- **Master-authorized-networks, network policy toggle, release channel
change, shielded-nodes toggle** — power-user controls; deferred.
- **Image type change, pool zone (locations) change** — both trigger
node recreations and are higher-risk; deferred until we add a
dedicated "replace pool" flow.

## Editable fields (MVP scope)

### Cluster

| Section | Field | UI | GCP API |
|---|---|---|---|
| Basic | resource labels | key/value list | `Clusters.SetResourceLabels` (separate endpoint, requires `LabelFingerprint`) |
| Maintenance | window mode | dropdown: none / daily | `Clusters.SetMaintenancePolicy` |
| Maintenance | daily start time | text "HH:MM" UTC (validated) | same |
| Observability | logging service | dropdown: none / SYSTEM / SYSTEM_AND_WORKLOAD | `Clusters.Update` (`DesiredLoggingService`) |
| Observability | monitoring service | dropdown: none / SYSTEM / SYSTEM_AND_WORKLOAD | `Clusters.Update` (`DesiredMonitoringService`) |

Cluster edit is allowed on **both Autopilot and Standard** — these
fields are top-level cluster knobs, not pool-specific. The existing
`canMutatePools()` Phase 2b guard does NOT apply.

### Node pool

| Section | Field | UI | GCP API |
|---|---|---|---|
| Labels | k8s labels (NodeConfig.Labels) | key/value list | `NodePools.Update` (NodeConfig.Labels) |
| Taints | taint list ({key, value, effect}) | repeating row: text key, text value, effect dropdown | `NodePools.Update` (NodeConfig.Taints) |
| Management | autoUpgrade | toggle | `NodePools.SetManagement` |
| Management | autoRepair | toggle | `NodePools.SetManagement` |
| Upgrade settings | strategy | dropdown: SURGE / BLUE_GREEN | `NodePools.Update` (UpgradeSettings) |
| Upgrade settings | max surge | int (0-N) | same |
| Upgrade settings | max unavailable | int (0-N) | same |

Pool edit is **Standard only** — Autopilot manages pools internally
and `canMutatePools()` returns false there.

## Architecture

### GCP layer (`internal/gcp/`)

Two new files:

```
gke_edit.go — edit method wrappers + edit-shape projections
gke_edit_test.go — request builder shape tests
```

Method surface (all return `Operation`):

```go
func (c *ContainerClient) UpdateClusterBasic(
ctx, projectID, location, clusterName, edit ClusterEdit,
) (Operation, error)

func (c *ContainerClient) SetClusterMaintenancePolicy(
ctx, projectID, location, clusterName, mw MaintenanceWindow,
) (Operation, error)

func (c *ContainerClient) UpdateNodePoolFields(
ctx, projectID, location, clusterName, poolName, edit NodePoolEdit,
) (Operation, error)

func (c *ContainerClient) SetNodePoolManagement(
ctx, projectID, location, clusterName, poolName, m NodePoolManagement,
) (Operation, error)
```

### Edit projections

Pointer fields encode "no change" (`nil`) vs "set to value" (`*v`).
Map/list fields are always whole-replacements when non-nil (the API
field mask handles this).

```go
type ClusterEdit struct {
Description *string
ResourceLabels *map[string]string
LoggingService *string
MonitoringService *string
}

type MaintenanceWindow struct {
Kind MaintenanceKind // "none" | "daily"
Daily string // "HH:MM" UTC, when Kind=="daily"
}

type NodePoolEdit struct {
Labels *map[string]string
Taints *[]NodeTaint
Tags *[]string // accepted in API but not exposed in MVP form
UpgradeSettings *UpgradeSettings
}

type NodeTaint struct {
Key, Value, Effect string // Effect: NO_SCHEDULE / PREFER_NO_SCHEDULE / NO_EXECUTE
}

type UpgradeSettings struct {
MaxSurge int64
MaxUnavailable int64
Strategy string // "SURGE" | "BLUE_GREEN"
}

type NodePoolManagement struct {
AutoUpgrade, AutoRepair bool
}
```

### UI layer (`internal/ui/views/`)

Two new views (form + diff preview, same two-state pattern as
`instance_editor.go`):

```
gke_cluster_edit.go — cluster edit form view
gke_cluster_edit_test.go
gke_node_pool_edit.go — node pool edit form view
gke_node_pool_edit_test.go
gke_edit_messages.go — Open / Request / Result / Canceled msgs
```

Each view holds a snapshot of the initial values at construction time
(captured from `gcp.ClusterDetails` / `gcp.NodePool`). On submit:

1. Compare each form field against its initial value.
2. Build a `ClusterEdit` / `NodePoolEdit` with non-nil pointers ONLY
for changed fields.
3. Compute a human-readable diff (per-field "old → new" lines).
4. Switch to `stateDiff`; show the diff for confirmation.
5. On Enter: emit `GKEClusterEditRequestMsg` / `GKENodePoolEditRequestMsg`,
transition to `stateSaving`.
6. On Esc: back to `stateForm`.

States: `stateForm` / `stateDiff` / `stateSaving`. Matches the existing
edit-view pattern in `instance_editor.go` and `cloudrun_edit.go`.

If no fields changed at submit, surface a "No changes" inline error
and stay in `stateForm` rather than spinning up a no-op API call.

### Two API calls per submit (when both basic + maintenance change)

`ClusterEdit` and `MaintenanceWindow` go to different endpoints. The
form may dirty fields in both groups in one submission. Approach:

- Issue them sequentially from the app handler.
- One task ID per submission: `gke-op:edit-cluster:<name>`.
- If basic+maintenance both dirty, run basic first → poll until DONE
→ run maintenance → poll until DONE → finish task. Status text
shows "Updating cluster X (1/2)..." while basic is polling.
- Per-call failure: short-circuit, set error on the cluster details
view, finish the task with the error.

The pool edit is similar: `NodePoolFields` (labels/taints/upgrade
settings) and `NodePoolManagement` (autoUpgrade/autoRepair) go to
different endpoints; same sequential-with-counter pattern.

This composite-poll lives in `app_navigation.go` next to `pollGKEOperation`:

```go
// pollGKEEditSequence runs a list of operation polls sequentially.
// Each entry is (op-name, op-location, status-prefix). Footer status
// updates as each step completes. On any step error, the whole
// sequence aborts and onError fires.
type gkeEditStep struct {
name, location string
label string // shown in footer e.g. "basic" / "maintenance"
}
```

### Key wiring

`internal/ui/views/gke_cluster_details.go`:

- **Overview tab + `e`**: open cluster edit. Allowed on Autopilot.
- **Node Pools tab + `e`**: open pool edit for the focused pool. Gated
by `canMutatePools()` (Standard only).

Per `.claude/rules/adding-new-views.md` checklist:

- Add `ViewGKEClusterEdit`, `ViewGKENodePoolEdit` view types.
- Wire `getCurrentViewModel`, `clearAllViews`, `updateViewSizes`,
`app_render.go renderCurrentView`, `updateSidebarActiveView`,
sidebar guards (include the new views in the GKE Clusters guard
list).
- `HasTextInputFocused` on both edit views (they contain text inputs).
- `IsMenuOpen` not relevant — these are full-screen views, not overlays.

### Message types (`gke_edit_messages.go`)

```go
// Nav signals from cluster details
type GKEClusterEditOpenMsg struct { ProjectID, Location, ClusterName string }
type GKENodePoolEditOpenMsg struct { ProjectID, Location, ClusterName, PoolName string }

// Form submits — handler dispatches to API.
type GKEClusterEditRequestMsg struct {
ProjectID, Location, ClusterName string
Basic *gcp.ClusterEdit // nil = no basic changes
Maintenance *gcp.MaintenanceWindow // nil = no maintenance changes
}

type GKENodePoolEditRequestMsg struct {
ProjectID, Location, ClusterName, PoolName string
Fields *gcp.NodePoolEdit // nil = no field changes
Management *gcp.NodePoolManagement // nil = no management changes
}

// Lifecycle
type GKEClusterEditCanceledMsg struct{}
type GKENodePoolEditCanceledMsg struct{}

type GKEClusterEditResultMsg struct {
TaskID, ClusterName string
Error error
}
type GKENodePoolEditResultMsg struct {
TaskID, ClusterName, PoolName string
Error error
}
```

## Edge cases + decisions

- **No-op submit** (no fields dirty): show "No changes to apply" inline
on the form; do not navigate to diff or call API.
- **Partial dirty** (e.g. only description changed): only the basic
call fires; maintenance call is skipped; one step in the sequence.
- **API path failure mid-sequence**: stop, refresh details (to capture
any partial state), surface error via cluster-details `SetError`.
- **Tags field not in pool form (MVP)**: `Tags` is part of the
`NodePoolEdit` struct so the API layer is complete, but the form
does not surface it. Future PR can add the section without churning
the API surface.
- **Map / list ordering**: labels and taints render alphabetical by
key for deterministic diff output (per `component-patterns.md`).
- **Diff layout**: two-column "Before │ After" for scalar fields, then
a "Changed entries" subsection for maps/lists showing added /
removed / changed lines (color-coded green / red / yellow).
- **Saving during a sequence**: footer task status updates as each
step completes. The user can cancel only between steps (Esc during
saving is forwarded to the parent for nav-back; the in-flight
operation continues server-side regardless).

## Reused infra (no new code)

- `pollGKEOperationOnce` + `gkeOperationPollResultMsg` from Phase 2b.
- `registerRunningTask` / `updateRunningTask` / `finishTask` footer
task framework.
- `borrowContainerClient` helper.
- `CreateViewBase` is NOT used here — edit views have a 3-state
machine (form / diff / saving), not the create-view's 2-state. They
hand-roll the lifecycle like `instance_editor.go` does.

## Open questions (call out at review time)

- Should taints support inline validation of effect (only the 3
GCP-allowed values)? Yes — dropdown forces it.
- Maintenance window kind: when changing from daily → none, the API
expects an empty MaintenancePolicy. Confirm with a small integration
test once we wire the form.
- Resource labels on Autopilot: should be supported per docs, but
worth verifying we don't get a 400 on submit. Track during smoke.
Loading
Loading