Validate, diff, confirm and apply, in that order every time - #66
Merged
Conversation
Closes #11. Closes #64. Every change to a cluster now goes through one pipeline (DESIGN.md §6.1), invoked on a manifest or on a directory of them: server dry run, diff, a dialog that restates where it is aimed, then the apply. Delete takes the same shape, its dry run listing the objects that would go rather than the file that names them. The preview is the operation minus the dry run, and that is held in code rather than promised in two doc comments: Validate and Apply build their argv through one function, and a test asserts the relationship — strip --dry-run=server from the validation and what remains must equal the apply, byte for byte. A preview that is not the operation is worse than no preview, because it is a preview the user trusts. The same is true of the delete pair, and of --server-side: a diff computed under different apply semantics is a diff of a different command, so the project's setting reaches every step. Protection is enforced in internal/app, before any process exists. A protected binding refuses to mutate unless the typed context matches the RESOLVED context exactly — no trimming, no case folding — and the resolved one is the point: in the layout scopes exist for, a file under prod/ is protected by a rule three directories up and targets a context the project as a whole does not, so checking the project default would ask for the wrong word and then accept it. The dialog collects the answer; it does not decide. A guard living only in a dialog is a guard the next caller gets to forget, and #14 is the next caller. Five bindings rather than three. KubeValidate is KubeApply under a dry run and KubeDeletePreview is KubeDelete under the same, so each pair could have been one method and a boolean — which would put the difference between previewing a deletion and performing one behind a flag the frontend passes. Two methods is the price of that condition not existing anywhere. Paths are confined before they leave the process. kubectl opens the file itself, outside the os.Root every other file operation in this repository runs through, so watch.Resolve proves the target is inside the worktree by stat-ing through that same root — the runtime check is the half fs.ValidPath cannot do, and it is what refuses a symlink created after the static one passed. Run log. Per project, in memory, this session: what ran, where, and what it said. Persistence is v2 and deliberately not started here — an audit trail is a file with a format, a retention rule and a promise about its contents, and a log that persisted without those decisions is a record people rely on before anyone has decided what it means. Known limit, filed rather than papered over: kubectl delete waits on finalizers, so deleting a namespace outlives the 60s deadline and reports as "timed out" — an error meaning no verdict — while the deletion was accepted. That is #65, a decision about what a partially-observed mutation should say, not a number to raise. Not streamed. The issue asks for the apply's output streamed to the run log; a Result arrives whole, and streaming needs a frame type on internal/stream that is more than this ticket. The captured output and the argv are shown. --- The blame column (#64) used to empty itself on the first keystroke and stay empty until a save, on the reasoning that git blame answers in line numbers and one insertion moves every line under it. That is true about line numbers and only about line numbers. The attribution does not go stale when someone types: the line they edited is theirs now, and the other four hundred are still whoever's they were. So entries are anchored to positions and moved by CodeMirror's own change mapping — the machinery that keeps a selection where it was left — and an entry stands while its line still reads exactly as git measured it. Everywhere else the line is in no commit, which is the same thing git says about a line it finds in the working tree and nowhere else, and both are marked: `uncommitted` in the column, and a dark yellow band across the code. The band is the mark that gets read, so it does not belong to the column. "Which of these lines have I changed" is asked while looking at the file, and the answer cannot live fourteen characters to the left in a gutter most people keep closed. That decoupled the read from the toggle: a blame is now taken for whatever file is open, one subprocess per file, refreshed on save. A file git cannot blame is an ordinary file to edit, so its refusal is only shown once the column is asked for. Both marks are built from one walk of the document into two range sets, positioned by the document and diffed by the same machinery. Nothing in the module reads the layout. The column had been built by CodeMirror's per-line callback, which hands you a laid-out block and makes you find the line yourself, with its own hand-written refresh predicate beside the band's — two paths over one rule, and a lookup whose two inputs have to be in step. They came apart anyway, and not there. Opening the column narrows the code by the width of the column; the code wraps; every line that wraps at one width and not the other changes height. Nothing notices, and it takes two things being true: gutters are inserted into the scroller beside the content, so adding one narrows the text without resizing the element the ResizeObserver watches — and that observer ignores anything within 75ms of an update anyway, which a reconfiguration is. The gutters kept heights from the old width while the text laid out at the new one and the two slid apart by a row per wrapped line, which reads as marks against the wrong lines and corrects itself on the scroll that finally forces a measure. showBlame now asks for that measure, which is the call CodeMirror's own resize handler makes. DESIGN.md §7 specified the hiding behaviour, so it is amended rather than quietly overruled. --- Ratchets, with the argument in the diff. maxAppMethods 32 -> 37, internal/app 900 -> 1150 LOC, internal/watch 1250 -> 1300 and 22 -> 23 exported. internal/app's new bindings share kube.go rather than taking a ninth file, which is what the package's file ceiling asks for and also what they are.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #66 +/- ##
==========================================
+ Coverage 94.64% 94.72% +0.07%
==========================================
Files 41 42 +1
Lines 2037 2179 +142
==========================================
+ Hits 1928 2064 +136
- Misses 68 71 +3
- Partials 41 44 +3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #11. Closes #64.
What this does
Every change to a cluster goes through one pipeline (DESIGN.md §6.1), invoked on a manifest or a directory of them: server dry run → diff → a dialog that restates where it is aimed → apply. Delete takes the same shape, its dry run listing the objects that would go rather than the file that names them.
The preview is the operation minus the dry run, and that is held in code rather than promised in two doc comments.
ValidateandApplybuild their argv through one function, and a test asserts the relationship: strip--dry-run=serverfrom the validation and what remains must equal the apply exactly. A preview that is not the operation is worse than no preview, because it is a preview the user trusts. Same for the delete pair, and for--server-side— a diff computed under different apply semantics is a diff of a different command, so the project's setting reaches every step.Protection is enforced in
internal/app, before any process exists. A protected binding refuses to mutate unless the typed context matches the resolved context exactly — no trimming, no case folding. The resolved one is the point: in the layout scopes exist for, a file underprod/is protected by a rule three directories up and targets a context the project as a whole does not, so checking the project default would ask for the wrong word and then accept it. The dialog collects the answer; it does not decide.Five bindings rather than three.
KubeValidateisKubeApplyunder a dry run andKubeDeletePreviewisKubeDeleteunder the same, so each pair could have been one method and a boolean — which would put the difference between previewing a deletion and performing one behind a flag the frontend passes. Two methods is the price of that condition not existing anywhere.Paths are confined before they leave the process. kubectl opens the file itself, outside the
os.Rootevery other file operation here runs through, sowatch.Resolveproves the target is inside the worktree by stat-ing through that same root. The runtime check is the halffs.ValidPathcannot do, and it is what refuses a symlink created after the static one passed.Run log: per project, in memory, this session. Persistence is v2 and deliberately not started — an audit trail is a file with a format, a retention rule and a promise about its contents, and a log that persisted without those decisions is a record people rely on before anyone has decided what it means.
Also fixes #64 — the blame column. It used to empty itself on the first keystroke and stay empty until a save, on the reasoning that
git blameanswers in line numbers and one insertion moves every line under it. True about line numbers and only about line numbers: the attribution does not go stale when someone types. Entries are now anchored to positions and moved by CodeMirror's own change mapping, and an entry stands while its line still reads exactly as git measured it. Everywhere else the line is in no commit — the same thing git says about a line it finds in the working tree and nowhere else — and both are marked:uncommittedin the column, and a dark yellow band across the code.The band does not belong to the column. "Which of these lines have I changed" is asked while looking at the file, and the answer cannot live fourteen characters to the left in a gutter most people keep closed. That decoupled the read from the toggle: a blame is taken for whatever file is open, one subprocess per file, refreshed on save. A file git cannot blame is an ordinary file to edit, so its refusal is only shown once the column is asked for.
DESIGN.md §7 specified the hiding behaviour, so it is amended rather than quietly overruled.
Two deviations, both deliberate
kubeexec.Resultarrives whole. Streaming needs a frame type oninternal/stream, which is more than this ticket. The full captured output and the argv are shown.kubectl deleteoutlives the 60s deadline. It waits on finalizers, so deleting a namespace reports as "timed out" — an error meaning no verdict — while the deletion was accepted. Filed as Cluster mutations share a 60s timeout that a delete routinely outlives #65 rather than papered over: it is a decision about what a partially-observed mutation should say, not a number to raise. The reasoning is beside the constant ininternal/kubeexec.How it was verified
make verifygreen: total coverage 96.4%, patch coverage 96.7% (floor 85%).Acceptance criteria from #11:
usePipeline.test.tsandPipelineDialog.test.tsx;diffVerdictkeeps exit 0 / 1 / >1 as three answers, because collapsing "there are changes" with "the command failed" puts a failure behind an Apply button.internal/app/pipeline_test.goruns the refusal table (nothing typed, wrong context, leading and trailing whitespace, wrong case, a prefix) and asserts twice over that no kubectl process was created, via a stub that would leave a marker if one had.pipeline.test.tsruns the same table against the frontend's copy of the rule, so the button can never be enabled on a looser condition than the backend's.Acceptance criteria from #64: typing inside a line leaves every other line's entry in place; inserting a line above moves entries down with the text still naming the right commit; deleting a line does not hand its entry to the line that took its place; undo restores the names without a re-read; no commit's name is ever shown against a line whose text differs from what git measured; a file with no blame read shows an empty column rather than a column of
uncommitted.One thing that is not tested, and needs your eyes
The marks were coming apart on screen — the column naming one line and the band another. It was not the marks. Opening the column narrows the code by the width of the column, the code wraps, and every line that wraps at one width and not the other changes height. Nothing notices, which takes two things both being true: gutters are inserted into the scroller beside the content, so adding one narrows the text without resizing the element CodeMirror's
ResizeObserverwatches — and that observer ignores anything within 75ms of an update anyway, which a reconfiguration is. The gutters kept heights from the old width while the text laid out at the new one, and the two slid apart by a row per wrapped line.showBlamenow callsview.requestMeasure(), which is the call CodeMirror's own resize handler makes.jsdom lays nothing out, so there is no wrapping to remeasure and no test can cover it. What is covered is the invariant underneath: two tests assert the column and the band describe exactly the same lines through an edit, a re-read, a second edit elsewhere, and an insertion that moves everything below it.
Ratchets raised, with the argument in the diff
maxAppMethods32 → 37,internal/app900 → 1150 LOC,internal/watch1250 → 1300 LOC and 22 → 23 exported.internal/app's new bindings sharekube.gorather than taking a ninth file, which is what the package's file ceiling asks for and also what they are.Checklist
Binding.argv, with the path inside--filename=so a value beginning with a dash cannot be read as a flag