<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kirill</title>
    <description>The latest articles on DEV Community by Kirill (@dudude).</description>
    <link>https://dev.to/dudude</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4101442%2Ff3332663-d820-4c6d-8ea3-801f64dff08a.png</url>
      <title>DEV Community: Kirill</title>
      <link>https://dev.to/dudude</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dudude"/>
    <language>en</language>
    <item>
      <title>"I don't know" is a state your UI probably doesn't have</title>
      <dc:creator>Kirill</dc:creator>
      <pubDate>Sun, 30 Aug 2026 21:29:26 +0000</pubDate>
      <link>https://dev.to/dudude/i-dont-know-is-a-state-your-ui-probably-doesnt-have-41bd</link>
      <guid>https://dev.to/dudude/i-dont-know-is-a-state-your-ui-probably-doesnt-have-41bd</guid>
      <description>&lt;p&gt;Someone read the source of &lt;a href="https://github.com/Dudude-bit/rubick" rel="noopener noreferrer"&gt;Rubick&lt;/a&gt;, the&lt;br&gt;
Kubernetes desktop client I maintain, and emailed me two security problems. The&lt;br&gt;
worse one:&lt;/p&gt;

&lt;p&gt;Draining a node evicts its pods. The eviction API is the part of Kubernetes&lt;br&gt;
that checks a PodDisruptionBudget, the object that says "never take the last&lt;br&gt;
healthy replica of this". A plain &lt;code&gt;DELETE&lt;/code&gt; checks nothing.&lt;/p&gt;

&lt;p&gt;My drain fell back to &lt;code&gt;DELETE&lt;/code&gt; whenever an eviction returned an error. Any&lt;br&gt;
error. And the UI passed the flag that enabled it on every single drain, so a&lt;br&gt;
budget would refuse an eviction and the pod was deleted anyway a moment later.&lt;/p&gt;

&lt;p&gt;That's a bug on its own. What sent me looking further was the dialog that was&lt;br&gt;
on screen while it happened:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The drain will not fail on these — it will wait, and keep waiting until&lt;br&gt;
another replica is ready somewhere else.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The words were right. &lt;code&gt;kubectl drain&lt;/code&gt; does exactly that. Someone had written&lt;br&gt;
that sentence carefully and the code underneath had never done it.&lt;/p&gt;

&lt;p&gt;I shipped the fix. A week later a user opened an issue with a screenshot: his&lt;br&gt;
gateway was working, his routes were carrying traffic, and my app had five of&lt;br&gt;
them in red.&lt;/p&gt;

&lt;p&gt;His setup runs on a private overlay network. The gateway there has no external&lt;br&gt;
address, because there is nowhere external to publish one. My code read an&lt;br&gt;
empty &lt;code&gt;status.addresses&lt;/code&gt; and said: &lt;em&gt;"Gateway &lt;code&gt;private&lt;/code&gt; has no address yet,&lt;br&gt;
traffic has nowhere to arrive."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;status.addresses&lt;/code&gt; is optional in the Gateway API spec. Plenty of&lt;br&gt;
implementations never fill it. I checked what my own code did by running it&lt;br&gt;
rather than reading it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;state: err | serving: false | servingKnown: true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last flag is the one that stings. It exists so that a verdict nobody&lt;br&gt;
checked doesn't render as checked. My app wasn't just wrong, it was sure.&lt;/p&gt;

&lt;p&gt;In the same issue he'd compared my output against another Kubernetes UI, which&lt;br&gt;
showed his cluster as entirely fine. I want to be careful here, because the&lt;br&gt;
obvious reading is that they got it right and I got it wrong. They didn't. The&lt;br&gt;
other tool never looked either; it just defaults to green instead of red. Both&lt;br&gt;
of us were reporting a fact nobody had established. Only one of us was going&lt;br&gt;
to get complained at for it.&lt;/p&gt;
&lt;h2&gt;
  
  
  The same mistake, twice more
&lt;/h2&gt;

&lt;p&gt;I fixed the address check and released. He came back: still red, one step&lt;br&gt;
further along.&lt;/p&gt;

&lt;p&gt;The route now stopped at the controller's verdict, and the reason my app gave&lt;br&gt;
was that either nothing claims the gateway's class or the controller isn't&lt;br&gt;
running. Both of those were contradicted by the two lines directly above them&lt;br&gt;
on his screen. The class &lt;em&gt;was&lt;/em&gt; claimed, by a named controller. The gateway&lt;br&gt;
&lt;em&gt;was&lt;/em&gt; programmed. The controller was there. It just doesn't write status for&lt;br&gt;
routes, which is common enough for the alpha kinds that I should have expected&lt;br&gt;
it.&lt;/p&gt;

&lt;p&gt;The third one arrived from the other direction, which is what made me stop&lt;br&gt;
treating these as separate bugs.&lt;/p&gt;

&lt;p&gt;A route rule can carry an &lt;code&gt;ExtensionRef&lt;/code&gt; filter and no backend. Envoy Gateway's&lt;br&gt;
direct-response works this way: the filter answers, no backend needed. Rubick&lt;br&gt;
called that broken. A contributor sent a patch, and the patch over-corrected&lt;br&gt;
into the opposite claim: &lt;em&gt;"an extension filter answers, no backends, and none&lt;br&gt;
needed."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Also not knowable. &lt;code&gt;ExtensionRef&lt;/code&gt; doesn't say what the filter does. A Kong&lt;br&gt;
plugin rate-limits and still needs somewhere to send the request, so a route&lt;br&gt;
with a plugin and a forgotten backend would now read as healthy.&lt;/p&gt;

&lt;p&gt;And the detail page had already been printing, four lines above:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;filters this app does not interpret: &lt;code&gt;KongPlugin.configuration.konghq.com/rate-limit&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One screen, one rule, both sentences. I can't interpret this. This answers by&lt;br&gt;
itself.&lt;/p&gt;
&lt;h2&gt;
  
  
  Three answers, two states
&lt;/h2&gt;

&lt;p&gt;There are three answers to "is this working?" Yes, no, and I couldn't see. My&lt;br&gt;
app had two, and every time the third one came up it got filed under "no".&lt;/p&gt;

&lt;p&gt;The reason it keeps happening is that absence looks like evidence. An empty&lt;br&gt;
list, a missing field, no status object: the data feels like an answer when it is&lt;br&gt;
only the shape of where one would go. The difference only shows up when somebody's setup is&lt;br&gt;
unusual enough to expose it, which in my case meant overlay networks and alpha&lt;br&gt;
API kinds.&lt;/p&gt;

&lt;p&gt;There's a less flattering reason too. Two states are cheaper to render. Green&lt;br&gt;
tick, red cross, done. A third one means another visual treatment, another&lt;br&gt;
sentence to write, and a verdict that admits what it doesn't cover.&lt;/p&gt;
&lt;h2&gt;
  
  
  Naming the unknown
&lt;/h2&gt;

&lt;p&gt;The fix was to put the third state in the type system rather than in prose.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TraceStepState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;err&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;warn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;off&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blind&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;blind&lt;/code&gt; means the step could not read its source, which is a different thing&lt;br&gt;
from having read it and found nothing. The overall verdict then carries the distinction as two booleans instead&lt;br&gt;
of one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;firstBroken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findIndex&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;err&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blind&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;who&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;machine&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;serving&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;firstBroken&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;servingKnown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;firstBroken&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;unread&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;serving&lt;/code&gt; is what I think. &lt;code&gt;servingKnown&lt;/code&gt; is whether anyone checked. A refusal&lt;br&gt;
still counts as known, because an error is an answer; only silence is unknown.&lt;br&gt;
On screen it's a dashed line rather than a red one, with a sentence saying what&lt;br&gt;
couldn't be read.&lt;/p&gt;

&lt;p&gt;All three bugs collapsed into the same fix:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;before&lt;/th&gt;
&lt;th&gt;after&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;no published address&lt;/td&gt;
&lt;td&gt;✗ traffic has nowhere to arrive&lt;/td&gt;
&lt;td&gt;? publishes no address&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;no route status&lt;/td&gt;
&lt;td&gt;✗ invisible to the data plane&lt;/td&gt;
&lt;td&gt;? controller wrote no verdict&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unread filter&lt;/td&gt;
&lt;td&gt;✗ nowhere to go / ✓ none needed&lt;/td&gt;
&lt;td&gt;? a filter is named, and no backend&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;His route now reads as serving with an unverified step in the middle. Not an&lt;br&gt;
alarm, and not a promise either.&lt;/p&gt;
&lt;h2&gt;
  
  
  The tests didn't notice any of this
&lt;/h2&gt;

&lt;p&gt;The tests for the ExtensionRef change asserted this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;say&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;filter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I replaced the catalogue string with &lt;strong&gt;"No filter answers this — the request&lt;br&gt;
has nowhere to go"&lt;/strong&gt;, which means the opposite of what shipped, and all 24&lt;br&gt;
tests stayed green.&lt;/p&gt;

&lt;p&gt;So now I break the code on purpose and check that the tests notice. That&lt;br&gt;
sounds like a ritual until it isn't: twice in one week my own sabotage failed&lt;br&gt;
to fail, and I only found out because I ran it.&lt;/p&gt;

&lt;p&gt;The first time, a rule about not chasing evicted pods' replacements turned out&lt;br&gt;
to be unobservable until I added a pod that tolerates the cordon. Without that&lt;br&gt;
one specimen in the scene, correct and broken code behaved identically and the&lt;br&gt;
test passed either way. The second time I'd written a guard for a case that&lt;br&gt;
couldn't be reached at all: an earlier step already errored and suppressed it.&lt;br&gt;
I deleted the guard. Code that looks like a safety net without being&lt;br&gt;
one is worse than none, because you stop checking.&lt;/p&gt;
&lt;h2&gt;
  
  
  Nine hours later
&lt;/h2&gt;

&lt;p&gt;I drafted the paragraph above at two in the afternoon. That evening the same&lt;br&gt;
shape came back, in code I had written that morning.&lt;/p&gt;

&lt;p&gt;A route can attach to a &lt;code&gt;ListenerSet&lt;/code&gt; instead of directly to a Gateway — the&lt;br&gt;
Gateway stays bare and every hostname and certificate lives in a per-app set.&lt;br&gt;
Rubick was filing those routes under "not judged here". I fixed it, checked it&lt;br&gt;
against a live cluster, took a screenshot, shipped it.&lt;/p&gt;

&lt;p&gt;The same bug was alive in the next code path over. Not because anyone forgot&lt;br&gt;
the rule — because of this, in the constructor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;listener_sets_known&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two callers read a Gateway and did not go on to merge the sets in. Both&lt;br&gt;
inherited a confident answer to a question nobody had asked. On a cluster where&lt;br&gt;
the ListenerSet sits there in &lt;code&gt;kubectl get&lt;/code&gt;, the graph reported it &lt;code&gt;Missing&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The forgotten call was the shallow half of it. What mattered was the default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;listener_sets_known&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only a real read sets it now, and forgetting produces "not looked at" — which&lt;br&gt;
the app already knows how to draw. I checked all three states on a live&lt;br&gt;
cluster: with the old default, &lt;code&gt;Missing&lt;/code&gt;; with the new default and still no&lt;br&gt;
merge, &lt;code&gt;NotChecked&lt;/code&gt;; with both, the route finds its Gateway.&lt;/p&gt;

&lt;p&gt;Naming the third state isn't enough if the default answer is the confident one.&lt;br&gt;
The question isn't whether you remember the rule. It's what the code says when&lt;br&gt;
you don't.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>rust</category>
      <category>testing</category>
      <category>devtools</category>
    </item>
  </channel>
</rss>
