<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://freshbrewed.science/feed.xml" rel="self" type="application/atom+xml" /><link href="https://freshbrewed.science/" rel="alternate" type="text/html" /><updated>2026-04-09T07:32:01+00:00</updated><id>https://freshbrewed.science/feed.xml</id><title type="html">Fresh/Brewed</title><subtitle>Tales from Cloudy McCloudface.
</subtitle><entry><title type="html">Gemma4, Claude and Pi</title><link href="https://freshbrewed.science/2026/04/09/gemma.html" rel="alternate" type="text/html" title="Gemma4, Claude and Pi" /><published>2026-04-09T01:01:01+00:00</published><updated>2026-04-09T01:01:01+00:00</updated><id>https://freshbrewed.science/2026/04/09/gemma</id><content type="html" xml:base="https://freshbrewed.science/2026/04/09/gemma.html"><![CDATA[<p>I had two things I wanted to tackle today: Comparing using Anthropic via <a href="https://github.com/badlogic/pi-mono">Pi CLI</a> versus Claude Code, then taking a look at Gemma4 - the new open model Google released last week.  In this article, I want to dig into <a href="https://ollama.com/library/gemma4">Gemma4</a> more thoroughly rather than rush an article just to get on the headline bandwagon.</p>

<p>As I’ll get into below, the first section (Pi vs Claude) came from a user request via <a href="https://www.linkedin.com/posts/isaacinmn_pi-coding-agent-share-7444716879790837760-kAya?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAAAAkg5wB9p5ZPIZqcc0aVS3My9TP21c1oQQ">LinkedIn</a>.  I tend to avoid Anthropic just due to price.</p>

<h1 id="anthropic-cli-vs-pi">Anthropic CLI vs Pi</h1>

<p><a href="https://www.linkedin.com/in/samhegge/">Sam Hegge</a> asked me if I could compare Claude Code versus Pi CLI as a comment on my last <a href="https://www.linkedin.com/posts/isaacinmn_pi-coding-agent-share-7444716879790837760-kAya?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAAAAkg5wB9p5ZPIZqcc0aVS3My9TP21c1oQQ">LI post</a> which was <a href="https://freshbrewed.science/2026/03/26/piagent.html">about the Pi agent</a> how it compared to Claude CLI.</p>

<p>I focus on Gemini CLI mostly because I’m generally Google-first, but also because Gemini AI Pro is covered and Anthropic usage tends to cost me.</p>

<p>That said, it’s a fair question - How might Pi work compared to Claude?  I’m willing to feed AI some coins to see.</p>

<h2 id="anthropic-setup-with-pi">Anthropic setup with Pi</h2>

<p>You just need to set an Anthropic key in your environment for Pi to pick up:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ export ANTHROPIC_API_KEY=sk-ant-api03-X-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
$ pi
</code></pre></div></div>

<p>Now when I ask for models, we can see the latest Anthropic ones listed:</p>

<p><a href="/content/images/2026/04/pi-01.png"><img src="/content/images/2026/04/pi-01.png" alt="/content/images/2026/04/pi-01.png" /></a></p>

<p>I picked the latest <code class="language-plaintext highlighter-rouge">claude-3-7-sonnet-latest</code> to use.</p>

<p>I actually have a desire to make some skills today, so I will actually use a Skill builder Skill for this activity.</p>

<p>I found a good one from Anthropic here: <a href="https://github.com/anthropics/skills/blob/main/skills/skill-creator/SKILL.md">https://github.com/anthropics/skills/blob/main/skills/skill-creator/SKILL.md</a></p>

<p>I actually already keep a local clone of Anthropic’s primary Skills library, so I just need to update it:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/skills/skills$ ls
algorithmic-art   canvas-design    docx             internal-comms  pdf   skill-creator      theme-factory          webapp-testing
brand-guidelines  doc-coauthoring  frontend-design  mcp-builder     pptx  slack-gif-creator  web-artifacts-builder  xlsx
builder@DESKTOP-QADGF36:~/Workspaces/skills/skills$ cd ..
builder@DESKTOP-QADGF36:~/Workspaces/skills$ git remote show origin
* remote origin
  Fetch URL: https://github.com/anthropics/skills.git
  Push  URL: https://github.com/anthropics/skills.git
  HEAD branch: main
  Remote branches:
    andibrae/create-top-level-namespace tracked
    klazuka/add-3p-notices              tracked
    klazuka/add-cc-instructions         tracked
    klazuka/add-cc-marketplace          tracked
    klazuka/doc-skills                  tracked
    klazuka/export                      tracked
    klazuka/export-20260203             new (next fetch will store in remotes/origin)
    klazuka/frontend-design-skill       tracked
    klazuka/pptx-cleanup                new (next fetch will store in remotes/origin)
    klazuka/spec                        tracked
    mahesh/add-to-readme                tracked
    mahesh/clarify-claude-code-install  tracked
    main                                tracked
    mattpic-ant/blog-small-fix          tracked
  Local branch configured for 'git pull':
    main merges with remote main
  Local ref configured for 'git push':
    main pushes to main (local out of date)
builder@DESKTOP-QADGF36:~/Workspaces/skills$ git pull
remote: Enumerating objects: 226, done.
remote: Counting objects: 100% (12/12), done.
remote: Total 226 (delta 12), reused 12 (delta 12), pack-reused 214 (from 1)
Receiving objects: 100% (226/226), 293.56 KiB | 2.39 MiB/s, done.
Resolving deltas: 100% (60/60), completed with 5 local objects.
From https://github.com/anthropics/skills
   69c0b1a..98669c1  main                    -&gt; origin/main
 * [new branch]      klazuka/export-20260203 -&gt; origin/klazuka/export-20260203
 * [new branch]      klazuka/pptx-cleanup    -&gt; origin/klazuka/pptx-cleanup
Updating 69c0b1a..98669c1
Fast-forward
 .claude-plugin/marketplace.json                                                             |   10 +
 skills/claude-api/LICENSE.txt                                                               |  202 ++
 skills/claude-api/SKILL.md                                                                  |  262 +++
 skills/claude-api/csharp/claude-api.md                                                      |  402 ++++
 skills/claude-api/curl/examples.md                                                          |  216 +++
 skills/claude-api/go/claude-api.md                                                          |  421 +++++
 skills/claude-api/java/claude-api.md                                                        |  432 +++++
 skills/claude-api/php/claude-api.md                                                         |  375 ++++
 skills/claude-api/python/agent-sdk/README.md                                                |  355 ++++
 skills/claude-api/python/agent-sdk/patterns.md                                              |  359 ++++
 skills/claude-api/python/claude-api/README.md                                               |  420 +++++
 skills/claude-api/python/claude-api/batches.md                                              |  185 ++
 skills/claude-api/python/claude-api/files-api.md                                            |  165 ++
 skills/claude-api/python/claude-api/streaming.md                                            |  162 ++
 skills/claude-api/python/claude-api/tool-use.md                                             |  590 ++++++
 skills/claude-api/ruby/claude-api.md                                                        |  113 ++
 skills/claude-api/shared/error-codes.md                                                     |  206 ++
 skills/claude-api/shared/live-sources.md                                                    |  121 ++
 skills/claude-api/shared/models.md                                                          |  119 ++
 skills/claude-api/shared/prompt-caching.md                                                  |  128 ++
 skills/claude-api/shared/tool-use-concepts.md                                               |  305 +++
 skills/claude-api/typescript/agent-sdk/README.md                                            |  297 +++
 skills/claude-api/typescript/agent-sdk/patterns.md                                          |  209 +++
 skills/claude-api/typescript/claude-api/README.md                                           |  333 ++++
 skills/claude-api/typescript/claude-api/batches.md                                          |  106 ++
 skills/claude-api/typescript/claude-api/files-api.md                                        |   98 +
 skills/claude-api/typescript/claude-api/streaming.md                                        |  178 ++
 skills/claude-api/typescript/claude-api/tool-use.md                                         |  527 ++++++
 skills/docx/SKILL.md                                                                        |  659 +++++--
 skills/docx/docx-js.md                                                                      |  350 ----
 skills/docx/ooxml.md                                                                        |  610 ------
 skills/docx/ooxml/scripts/pack.py                                                           |  159 --
 skills/docx/ooxml/scripts/unpack.py                                                         |   29 -
 skills/docx/ooxml/scripts/validate.py                                                       |   69 -
 skills/docx/ooxml/scripts/validation/docx.py                                                |  274 ---
 skills/docx/scripts/__init__.py                                                             |    2 +-
 skills/docx/scripts/accept_changes.py                                                       |  135 ++
 skills/docx/scripts/comment.py                                                              |  318 ++++
 skills/docx/scripts/document.py                                                             | 1276 -------------
 skills/docx/scripts/office/helpers/__init__.py                                              |    0
 skills/docx/scripts/office/helpers/merge_runs.py                                            |  199 ++
 skills/docx/scripts/office/helpers/simplify_redlines.py                                     |  197 ++
 skills/docx/scripts/office/pack.py                                                          |  159 ++
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-chart.xsd             |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd      |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd           |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd      |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-main.xsd              |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-picture.xsd           |    0
 .../docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd   |    0
 .../{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd     |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/pml.xsd                   |    0
 .../office}/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd                |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd   |    0
 .../docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd |    0
 .../office}/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd                  |    0
 .../office}/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd                |    0
 .../office}/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd                 |    0
 .../office}/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd               |    0
 .../office}/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd           |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-math.xsd           |    0
 .../{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd  |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/sml.xsd                   |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-main.xsd              |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd     |    0
 .../docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd  |    0
 .../docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd   |    0
 .../{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd     |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/wml.xsd                   |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/xml.xsd                   |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-contentTypes.xsd       |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-coreProperties.xsd     |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-digSig.xsd             |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-relationships.xsd      |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/mce/mc.xsd                                    |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-2010.xsd                        |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-2012.xsd                        |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-2018.xsd                        |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-cex-2018.xsd                    |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-cid-2016.xsd                    |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-sdtdatahash-2020.xsd            |    0
 skills/docx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-symex-2015.xsd                  |    0
 skills/docx/scripts/office/soffice.py                                                       |  183 ++
 skills/docx/scripts/office/unpack.py                                                        |  132 ++
 skills/docx/scripts/office/validate.py                                                      |  111 ++
 skills/docx/{ooxml/scripts/validation =&gt; scripts/office/validators}/__init__.py             |    0
 skills/{pptx/ooxml/scripts/validation =&gt; docx/scripts/office/validators}/base.py            |  310 +---
 skills/docx/scripts/office/validators/docx.py                                               |  446 +++++
 skills/{pptx/ooxml/scripts/validation =&gt; docx/scripts/office/validators}/pptx.py            |   44 +-
 skills/docx/{ooxml/scripts/validation =&gt; scripts/office/validators}/redlining.py            |   74 +-
 skills/docx/scripts/templates/comments.xml                                                  |    4 +-
 skills/docx/scripts/templates/commentsExtended.xml                                          |    4 +-
 skills/docx/scripts/templates/commentsExtensible.xml                                        |    4 +-
 skills/docx/scripts/templates/commentsIds.xml                                               |    4 +-
 skills/docx/scripts/templates/people.xml                                                    |    4 +-
 skills/docx/scripts/utilities.py                                                            |  374 ----
 skills/pdf/SKILL.md                                                                         |   34 +-
 skills/pdf/forms.md                                                                         |  283 ++-
 skills/pdf/scripts/check_bounding_boxes.py                                                  |    5 -
 skills/pdf/scripts/check_bounding_boxes_test.py                                             |  226 ---
 skills/pdf/scripts/check_fillable_fields.py                                                 |    1 -
 skills/pdf/scripts/convert_pdf_to_images.py                                                 |    2 -
 skills/pdf/scripts/create_validation_image.py                                               |    4 -
 skills/pdf/scripts/extract_form_field_info.py                                               |   32 +-
 skills/pdf/scripts/extract_form_structure.py                                                |  115 ++
 skills/pdf/scripts/fill_fillable_fields.py                                                  |   16 -
 skills/pdf/scripts/fill_pdf_form_with_annotations.py                                        |   61 +-
 skills/pptx/SKILL.md                                                                        |  652 ++-----
 skills/pptx/editing.md                                                                      |  205 ++
 skills/pptx/html2pptx.md                                                                    |  625 -------
 skills/pptx/ooxml.md                                                                        |  427 -----
 skills/pptx/ooxml/scripts/pack.py                                                           |  159 --
 skills/pptx/ooxml/scripts/unpack.py                                                         |   29 -
 skills/pptx/ooxml/scripts/validate.py                                                       |   69 -
 skills/pptx/ooxml/scripts/validation/docx.py                                                |  274 ---
 skills/pptx/pptxgenjs.md                                                                    |  420 +++++
 skills/pptx/scripts/__init__.py                                                             |    0
 skills/pptx/scripts/add_slide.py                                                            |  195 ++
 skills/pptx/scripts/clean.py                                                                |  286 +++
 skills/pptx/scripts/html2pptx.js                                                            |  979 ----------
 skills/pptx/scripts/inventory.py                                                            | 1020 ----------
 skills/pptx/scripts/office/helpers/__init__.py                                              |    0
 skills/pptx/scripts/office/helpers/merge_runs.py                                            |  199 ++
 skills/pptx/scripts/office/helpers/simplify_redlines.py                                     |  197 ++
 skills/pptx/scripts/office/pack.py                                                          |  159 ++
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-chart.xsd             |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd      |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd           |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd      |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-main.xsd              |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-picture.xsd           |    0
 .../pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd   |    0
 .../{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd     |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/pml.xsd                   |    0
 .../office}/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd                |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd   |    0
 .../pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd |    0
 .../office}/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd                  |    0
 .../office}/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd                |    0
 .../office}/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd                 |    0
 .../office}/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd               |    0
 .../office}/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd           |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-math.xsd           |    0
 .../{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd  |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/sml.xsd                   |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-main.xsd              |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd     |    0
 .../pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd  |    0
 .../pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd   |    0
 .../{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd     |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/wml.xsd                   |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/xml.xsd                   |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-contentTypes.xsd       |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-coreProperties.xsd     |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-digSig.xsd             |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-relationships.xsd      |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/mce/mc.xsd                                    |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-2010.xsd                        |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-2012.xsd                        |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-2018.xsd                        |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-cex-2018.xsd                    |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-cid-2016.xsd                    |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-sdtdatahash-2020.xsd            |    0
 skills/pptx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-symex-2015.xsd                  |    0
 skills/pptx/scripts/office/soffice.py                                                       |  183 ++
 skills/pptx/scripts/office/unpack.py                                                        |  132 ++
 skills/pptx/scripts/office/validate.py                                                      |  111 ++
 skills/pptx/{ooxml/scripts/validation =&gt; scripts/office/validators}/__init__.py             |    0
 skills/{docx/ooxml/scripts/validation =&gt; pptx/scripts/office/validators}/base.py            |  310 +---
 skills/pptx/scripts/office/validators/docx.py                                               |  446 +++++
 skills/{docx/ooxml/scripts/validation =&gt; pptx/scripts/office/validators}/pptx.py            |   44 +-
 skills/pptx/{ooxml/scripts/validation =&gt; scripts/office/validators}/redlining.py            |   74 +-
 skills/pptx/scripts/rearrange.py                                                            |  231 ---
 skills/pptx/scripts/replace.py                                                              |  385 ----
 skills/pptx/scripts/thumbnail.py                                                            |  399 ++--
 skills/skill-creator/SKILL.md                                                               |  555 +++---
 skills/skill-creator/agents/analyzer.md                                                     |  274 +++
 skills/skill-creator/agents/comparator.md                                                   |  202 ++
 skills/skill-creator/agents/grader.md                                                       |  223 +++
 skills/skill-creator/assets/eval_review.html                                                |  146 ++
 skills/skill-creator/eval-viewer/generate_review.py                                         |  471 +++++
 skills/skill-creator/eval-viewer/viewer.html                                                | 1325 +++++++++++++
 skills/skill-creator/references/output-patterns.md                                          |   82 -
 skills/skill-creator/references/schemas.md                                                  |  430 +++++
 skills/skill-creator/references/workflows.md                                                |   28 -
 skills/skill-creator/scripts/__init__.py                                                    |    0
 skills/skill-creator/scripts/aggregate_benchmark.py                                         |  401 ++++
 skills/skill-creator/scripts/generate_report.py                                             |  326 ++++
 skills/skill-creator/scripts/improve_description.py                                         |  247 +++
 skills/skill-creator/scripts/init_skill.py                                                  |  303 ---
 skills/skill-creator/scripts/package_skill.py                                               |   40 +-
 skills/skill-creator/scripts/quick_validate.py                                              |   14 +-
 skills/skill-creator/scripts/run_eval.py                                                    |  310 ++++
 skills/skill-creator/scripts/run_loop.py                                                    |  328 ++++
 skills/skill-creator/scripts/utils.py                                                       |   47 +
 skills/xlsx/SKILL.md                                                                        |   21 +-
 skills/xlsx/recalc.py                                                                       |  178 --
 skills/xlsx/scripts/office/helpers/__init__.py                                              |    0
 skills/xlsx/scripts/office/helpers/merge_runs.py                                            |  199 ++
 skills/xlsx/scripts/office/helpers/simplify_redlines.py                                     |  197 ++
 skills/xlsx/scripts/office/pack.py                                                          |  159 ++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd                        | 1499 +++++++++++++++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd                 |  146 ++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd                      | 1085 +++++++++++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd                 |   11 +
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd                         | 3081 ++++++++++++++++++++++++++++++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd                      |   23 +
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd           |  185 ++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd        |  287 +++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd                              | 1676 +++++++++++++++++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd |   28 +
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd              |  144 ++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd         |  174 ++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd   |   25 +
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd |   18 +
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd  |   59 +
 .../xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd   |   56 +
 .../scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd    |  195 ++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd                      |  582 ++++++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd     |   25 +
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd                              | 4439 ++++++++++++++++++++++++++++++++++++++++++++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd                         |  570 ++++++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd                |  509 +++++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd          |   12 +
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd           |  108 ++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd        |   96 +
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd                              | 3646 ++++++++++++++++++++++++++++++++++++
 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd                              |  116 ++
 skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd                  |   42 +
 skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd                |   50 +
 skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd                        |   49 +
 skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd                 |   33 +
 skills/xlsx/scripts/office/schemas/mce/mc.xsd                                               |   75 +
 skills/xlsx/scripts/office/schemas/microsoft/wml-2010.xsd                                   |  560 ++++++
 skills/xlsx/scripts/office/schemas/microsoft/wml-2012.xsd                                   |   67 +
 skills/xlsx/scripts/office/schemas/microsoft/wml-2018.xsd                                   |   14 +
 skills/xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd                               |   20 +
 skills/xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd                               |   13 +
 skills/xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd                       |    4 +
 skills/xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd                             |    8 +
 skills/xlsx/scripts/office/soffice.py                                                       |  183 ++
 skills/xlsx/scripts/office/unpack.py                                                        |  132 ++
 skills/xlsx/scripts/office/validate.py                                                      |  111 ++
 skills/xlsx/scripts/office/validators/__init__.py                                           |   15 +
 skills/xlsx/scripts/office/validators/base.py                                               |  847 +++++++++
 skills/xlsx/scripts/office/validators/docx.py                                               |  446 +++++
 skills/xlsx/scripts/office/validators/pptx.py                                               |  275 +++
 skills/xlsx/scripts/office/validators/redlining.py                                          |  247 +++
 skills/xlsx/scripts/recalc.py                                                               |  184 ++
 249 files changed, 41029 insertions(+), 10062 deletions(-)
 create mode 100644 skills/claude-api/LICENSE.txt
 create mode 100644 skills/claude-api/SKILL.md
 create mode 100644 skills/claude-api/csharp/claude-api.md
 create mode 100644 skills/claude-api/curl/examples.md
 create mode 100644 skills/claude-api/go/claude-api.md
 create mode 100644 skills/claude-api/java/claude-api.md
 create mode 100644 skills/claude-api/php/claude-api.md
 create mode 100644 skills/claude-api/python/agent-sdk/README.md
 create mode 100644 skills/claude-api/python/agent-sdk/patterns.md
 create mode 100644 skills/claude-api/python/claude-api/README.md
 create mode 100644 skills/claude-api/python/claude-api/batches.md
 create mode 100644 skills/claude-api/python/claude-api/files-api.md
 create mode 100644 skills/claude-api/python/claude-api/streaming.md
 create mode 100644 skills/claude-api/python/claude-api/tool-use.md
 create mode 100644 skills/claude-api/ruby/claude-api.md
 create mode 100644 skills/claude-api/shared/error-codes.md
 create mode 100644 skills/claude-api/shared/live-sources.md
 create mode 100644 skills/claude-api/shared/models.md
 create mode 100644 skills/claude-api/shared/prompt-caching.md
 create mode 100644 skills/claude-api/shared/tool-use-concepts.md
 create mode 100644 skills/claude-api/typescript/agent-sdk/README.md
 create mode 100644 skills/claude-api/typescript/agent-sdk/patterns.md
 create mode 100644 skills/claude-api/typescript/claude-api/README.md
 create mode 100644 skills/claude-api/typescript/claude-api/batches.md
 create mode 100644 skills/claude-api/typescript/claude-api/files-api.md
 create mode 100644 skills/claude-api/typescript/claude-api/streaming.md
 create mode 100644 skills/claude-api/typescript/claude-api/tool-use.md
 delete mode 100644 skills/docx/docx-js.md
 delete mode 100644 skills/docx/ooxml.md
 delete mode 100755 skills/docx/ooxml/scripts/pack.py
 delete mode 100755 skills/docx/ooxml/scripts/unpack.py
 delete mode 100755 skills/docx/ooxml/scripts/validate.py
 delete mode 100644 skills/docx/ooxml/scripts/validation/docx.py
 create mode 100755 skills/docx/scripts/accept_changes.py
 create mode 100755 skills/docx/scripts/comment.py
 delete mode 100755 skills/docx/scripts/document.py
 create mode 100644 skills/docx/scripts/office/helpers/__init__.py
 create mode 100644 skills/docx/scripts/office/helpers/merge_runs.py
 create mode 100644 skills/docx/scripts/office/helpers/simplify_redlines.py
 create mode 100755 skills/docx/scripts/office/pack.py
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-chart.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-main.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-picture.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/pml.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-math.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/sml.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-main.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/wml.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/xml.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-contentTypes.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-coreProperties.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-digSig.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-relationships.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/mce/mc.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-2010.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-2012.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-2018.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-cex-2018.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-cid-2016.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-sdtdatahash-2020.xsd (100%)
 rename skills/docx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-symex-2015.xsd (100%)
 create mode 100644 skills/docx/scripts/office/soffice.py
 create mode 100755 skills/docx/scripts/office/unpack.py
 create mode 100755 skills/docx/scripts/office/validate.py
 rename skills/docx/{ooxml/scripts/validation =&gt; scripts/office/validators}/__init__.py (100%)
 rename skills/{pptx/ooxml/scripts/validation =&gt; docx/scripts/office/validators}/base.py (71%)
 create mode 100644 skills/docx/scripts/office/validators/docx.py
 rename skills/{pptx/ooxml/scripts/validation =&gt; docx/scripts/office/validators}/pptx.py (79%)
 rename skills/docx/{ooxml/scripts/validation =&gt; scripts/office/validators}/redlining.py (72%)
 delete mode 100755 skills/docx/scripts/utilities.py
 delete mode 100644 skills/pdf/scripts/check_bounding_boxes_test.py
 create mode 100755 skills/pdf/scripts/extract_form_structure.py
 create mode 100644 skills/pptx/editing.md
 delete mode 100644 skills/pptx/html2pptx.md
 delete mode 100644 skills/pptx/ooxml.md
 delete mode 100755 skills/pptx/ooxml/scripts/pack.py
 delete mode 100755 skills/pptx/ooxml/scripts/unpack.py
 delete mode 100755 skills/pptx/ooxml/scripts/validate.py
 delete mode 100644 skills/pptx/ooxml/scripts/validation/docx.py
 create mode 100644 skills/pptx/pptxgenjs.md
 create mode 100644 skills/pptx/scripts/__init__.py
 create mode 100755 skills/pptx/scripts/add_slide.py
 create mode 100755 skills/pptx/scripts/clean.py
 delete mode 100755 skills/pptx/scripts/html2pptx.js
 delete mode 100755 skills/pptx/scripts/inventory.py
 create mode 100644 skills/pptx/scripts/office/helpers/__init__.py
 create mode 100644 skills/pptx/scripts/office/helpers/merge_runs.py
 create mode 100644 skills/pptx/scripts/office/helpers/simplify_redlines.py
 create mode 100755 skills/pptx/scripts/office/pack.py
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-chart.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-main.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-picture.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/pml.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-math.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/sml.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-main.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/wml.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ISO-IEC29500-4_2016/xml.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-contentTypes.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-coreProperties.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-digSig.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/ecma/fouth-edition/opc-relationships.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/mce/mc.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-2010.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-2012.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-2018.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-cex-2018.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-cid-2016.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-sdtdatahash-2020.xsd (100%)
 rename skills/pptx/{ooxml =&gt; scripts/office}/schemas/microsoft/wml-symex-2015.xsd (100%)
 create mode 100644 skills/pptx/scripts/office/soffice.py
 create mode 100755 skills/pptx/scripts/office/unpack.py
 create mode 100755 skills/pptx/scripts/office/validate.py
 rename skills/pptx/{ooxml/scripts/validation =&gt; scripts/office/validators}/__init__.py (100%)
 rename skills/{docx/ooxml/scripts/validation =&gt; pptx/scripts/office/validators}/base.py (71%)
 create mode 100644 skills/pptx/scripts/office/validators/docx.py
 rename skills/{docx/ooxml/scripts/validation =&gt; pptx/scripts/office/validators}/pptx.py (79%)
 rename skills/pptx/{ooxml/scripts/validation =&gt; scripts/office/validators}/redlining.py (72%)
 delete mode 100755 skills/pptx/scripts/rearrange.py
 delete mode 100755 skills/pptx/scripts/replace.py
 create mode 100644 skills/skill-creator/agents/analyzer.md
 create mode 100644 skills/skill-creator/agents/comparator.md
 create mode 100644 skills/skill-creator/agents/grader.md
 create mode 100644 skills/skill-creator/assets/eval_review.html
 create mode 100644 skills/skill-creator/eval-viewer/generate_review.py
 create mode 100644 skills/skill-creator/eval-viewer/viewer.html
 delete mode 100644 skills/skill-creator/references/output-patterns.md
 create mode 100644 skills/skill-creator/references/schemas.md
 delete mode 100644 skills/skill-creator/references/workflows.md
 create mode 100644 skills/skill-creator/scripts/__init__.py
 create mode 100755 skills/skill-creator/scripts/aggregate_benchmark.py
 create mode 100755 skills/skill-creator/scripts/generate_report.py
 create mode 100755 skills/skill-creator/scripts/improve_description.py
 delete mode 100755 skills/skill-creator/scripts/init_skill.py
 create mode 100755 skills/skill-creator/scripts/run_eval.py
 create mode 100755 skills/skill-creator/scripts/run_loop.py
 create mode 100644 skills/skill-creator/scripts/utils.py
 delete mode 100644 skills/xlsx/recalc.py
 create mode 100644 skills/xlsx/scripts/office/helpers/__init__.py
 create mode 100644 skills/xlsx/scripts/office/helpers/merge_runs.py
 create mode 100644 skills/xlsx/scripts/office/helpers/simplify_redlines.py
 create mode 100755 skills/xlsx/scripts/office/pack.py
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/mce/mc.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/microsoft/wml-2010.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/microsoft/wml-2012.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/microsoft/wml-2018.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd
 create mode 100644 skills/xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd
 create mode 100644 skills/xlsx/scripts/office/soffice.py
 create mode 100755 skills/xlsx/scripts/office/unpack.py
 create mode 100755 skills/xlsx/scripts/office/validate.py
 create mode 100644 skills/xlsx/scripts/office/validators/__init__.py
 create mode 100644 skills/xlsx/scripts/office/validators/base.py
 create mode 100644 skills/xlsx/scripts/office/validators/docx.py
 create mode 100644 skills/xlsx/scripts/office/validators/pptx.py
 create mode 100644 skills/xlsx/scripts/office/validators/redlining.py
 create mode 100755 skills/xlsx/scripts/recalc.py
builder@DESKTOP-QADGF36:~/Workspaces/skills$ ls
README.md  THIRD_PARTY_NOTICES.md  skills  spec  template
builder@DESKTOP-QADGF36:~/Workspaces/skills$ ls skills/
algorithmic-art   canvas-design  doc-coauthoring  frontend-design  mcp-builder  pptx           slack-gif-creator  web-artifacts-builder  xlsx
brand-guidelines  claude-api     docx             internal-comms   pdf          skill-creator  theme-factory      webapp-testing
</code></pre></div></div>

<p>I noticed since I updated last, they removed <code class="language-plaintext highlighter-rouge">claude-api</code>.  Wonder if the leaks of their code had anything to do with that.</p>

<p>My mode of using skills is to sym-link or copy them in the more global <code class="language-plaintext highlighter-rouge">~/.agent/skills</code></p>

<p>I’ll go ahead and link that <code class="language-plaintext highlighter-rouge">skill creator</code> there</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ln -s /home/builder/Workspaces/skills/skills/skill-creator /home/builder/.agents/skills/skill-creator
</code></pre></div></div>

<p>My second habit is to aggregate my newly created skills into their own private GIT repo</p>

<p><a href="/content/images/2026/04/pi-02.png"><img src="/content/images/2026/04/pi-02.png" alt="/content/images/2026/04/pi-02.png" /></a></p>

<p>Since I actually care about these now, I’ll do a backup to Codeberg (always good to do 2 homes for GIT)</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/forgejo-01.mp4" type="video/mp4" />
</video>

<p>Now that that is sorted, let’s clone our own agentskills repo to build something</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces$ git clone https://forgejo.freshbrewed.science/builderadmin/agentskills.git
Cloning into 'agentskills'...
remote: Enumerating objects: 11, done.
remote: Counting objects: 100% (11/11), done.
remote: Compressing objects: 100% (9/9), done.
remote: Total 11 (delta 1), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (11/11), 4.07 KiB | 4.07 MiB/s, done.
Resolving deltas: 100% (1/1), done.
builder@DESKTOP-QADGF36:~/Workspaces$ cd agentskills/
builder@DESKTOP-QADGF36:~/Workspaces/agentskills$ nvm use lts/jod
Now using node v22.22.0 (npm v10.9.4)
</code></pre></div></div>

<p>I fired up Pi and tried to load my skill.. seems i need to feed the pig…</p>

<p><a href="/content/images/2026/04/pi-03.png"><img src="/content/images/2026/04/pi-03.png" alt="/content/images/2026/04/pi-03.png" /></a></p>

<p>I have auto-reload disabled so when it’s out it’s out. So i fed it a $20</p>

<p><a href="/content/images/2026/04/pi-04.png"><img src="/content/images/2026/04/pi-04.png" alt="/content/images/2026/04/pi-04.png" /></a></p>

<p>Anthropic also does this use it or lose it thing</p>

<p><a href="/content/images/2026/04/pi-05.png"><img src="/content/images/2026/04/pi-05.png" alt="/content/images/2026/04/pi-05.png" /></a></p>

<p>So that’s another reason I’m not a giant fan of them, even if their tooling is pretty good.</p>

<p>My next issue was Sonnet 3.7 isn’t available.. I think my eyes just screwed that up seeing the “.7”.. 4.6 is the latest, not 3.7</p>

<p><a href="/content/images/2026/04/pi-06.png"><img src="/content/images/2026/04/pi-06.png" alt="/content/images/2026/04/pi-06.png" /></a></p>

<p>I can use the Skill creator with “/skill:skill-creator” - this makes Pi definitely load that creator skill for our next step.</p>

<p>Loading the skill already cost me 7c</p>

<p><a href="/content/images/2026/04/pi-07.png"><img src="/content/images/2026/04/pi-07.png" alt="/content/images/2026/04/pi-07.png" /></a></p>

<p>Let’s now build a skill out with Sonnet 4.6</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/pi-08.mp4" type="video/mp4" />
</video>

<p>As we can see, that took about 5 minutes with Pi and cost about US$0.395</p>

<p><a href="/content/images/2026/04/pi-09.png"><img src="/content/images/2026/04/pi-09.png" alt="/content/images/2026/04/pi-09.png" /></a></p>

<h2 id="claude-native">Claude native</h2>

<p>Let’s now fire up Claude natively in the same directory</p>

<p><a href="/content/images/2026/04/claudecode-01.png"><img src="/content/images/2026/04/claudecode-01.png" alt="/content/images/2026/04/claudecode-01.png" /></a></p>

<p>As we can see, it can create a new Helm Chart skill, but it loves to launch agents in Parallel which I fear is going to cost me.</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/claudecode-02.mp4" type="video/mp4" />
</video>

<p>it finished</p>

<p><a href="/content/images/2026/04/claudecode-03.png"><img src="/content/images/2026/04/claudecode-03.png" alt="/content/images/2026/04/claudecode-03.png" /></a></p>

<p>And it did build out a skill</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/agentskills$ tree ./helm-charts-workspace/
./helm-charts-workspace/
└── iteration-1
    ├── benchmark.json
    ├── benchmark.md
    ├── eval-1
    │   ├── eval_metadata.json
    │   ├── with_skill
    │   │   └── run-1
    │   │       ├── grading.json
    │   │       ├── outputs
    │   │       │   ├── Chart.yaml
    │   │       │   ├── templates
    │   │       │   │   ├── NOTES.txt
    │   │       │   │   ├── _helpers.tpl
    │   │       │   │   ├── deployment.yaml
    │   │       │   │   ├── ingress.yaml
    │   │       │   │   ├── namespace.yaml
    │   │       │   │   ├── pvc.yaml
    │   │       │   │   └── service.yaml
    │   │       │   └── values.yaml
    │   │       └── timing.json
    │   └── without_skill
    │       └── run-1
    │           ├── grading.json
    │           ├── outputs
    │           │   ├── Chart.yaml
    │           │   ├── templates
    │           │   │   ├── NOTES.txt
    │           │   │   ├── _helpers.tpl
    │           │   │   ├── deployment.yaml
    │           │   │   ├── ingress.yaml
    │           │   │   ├── namespace.yaml
    │           │   │   ├── pvc.yaml
    │           │   │   └── service.yaml
    │           │   └── values.yaml
    │           └── timing.json
    ├── eval-2
    │   ├── eval_metadata.json
    │   ├── with_skill
    │   │   └── run-1
    │   │       ├── grading.json
    │   │       ├── outputs
    │   │       │   ├── Chart.yaml
    │   │       │   ├── templates
    │   │       │   │   ├── NOTES.txt
    │   │       │   │   ├── _helpers.tpl
    │   │       │   │   ├── deployment.yaml
    │   │       │   │   └── service.yaml
    │   │       │   └── values.yaml
    │   │       └── timing.json
    │   └── without_skill
    │       └── run-1
    │           ├── grading.json
    │           ├── outputs
    │           │   ├── Chart.yaml
    │           │   ├── templates
    │           │   │   ├── NOTES.txt
    │           │   │   ├── _helpers.tpl
    │           │   │   ├── deployment.yaml
    │           │   │   └── service.yaml
    │           │   └── values.yaml
    │           └── timing.json
    └── eval-3
        ├── eval_metadata.json
        ├── with_skill
        │   └── run-1
        │       ├── grading.json
        │       ├── outputs
        │       │   ├── templates
        │       │   │   ├── ingress.yaml
        │       │   │   └── namespace.yaml
        │       │   └── values_ingress_section.yaml
        │       └── timing.json
        └── without_skill
            └── run-1
                ├── grading.json
                ├── outputs
                │   ├── templates
                │   │   ├── ingress.yaml
                │   │   └── namespace.yaml
                │   └── values_ingress_section.yaml
                └── timing.json

28 directories, 53 files
</code></pre></div></div>

<p>But cost me at least $2.30</p>

<p><a href="/content/images/2026/04/claudecode-04.png"><img src="/content/images/2026/04/claudecode-04.png" alt="/content/images/2026/04/claudecode-04.png" /></a></p>

<p>I think that’s why heavy users quickly switch from the free model with pay-per-token to a Claude Pro or Max plan</p>

<p><a href="/content/images/2026/04/claudecode-05.png"><img src="/content/images/2026/04/claudecode-05.png" alt="/content/images/2026/04/claudecode-05.png" /></a></p>

<h1 id="gemma-4">Gemma 4</h1>

<p>I’m on my Windows box at the moment, so let’s pull down Gemma4 which dropped in Ollama just last night</p>

<p><a href="/content/images/2026/04/gemma4-01.png"><img src="/content/images/2026/04/gemma4-01.png" alt="/content/images/2026/04/gemma4-01.png" /></a></p>

<p>While it would appear I could download with my Ollama</p>

<p><a href="/content/images/2026/04/gemma4-02.png"><img src="/content/images/2026/04/gemma4-02.png" alt="/content/images/2026/04/gemma4-02.png" /></a></p>

<p>I’m prompted to upgrade them moment i try and use it</p>

<p><a href="/content/images/2026/04/gemma4-04.png"><img src="/content/images/2026/04/gemma4-04.png" alt="/content/images/2026/04/gemma4-04.png" /></a></p>

<p>In Windows, I don’t know a great way to just tell it to pull a model other than launch from system tray and try and use it.  The first time you do it downloads the model of which you seek</p>

<p><a href="/content/images/2026/04/gemma4-03.png"><img src="/content/images/2026/04/gemma4-03.png" alt="/content/images/2026/04/gemma4-03.png" /></a></p>

<p>Let’s ask a coding question about Perl and see what it comes back with.  I’ll pause a few times, but leave the timer up</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/gemma4-05.mp4" type="video/mp4" />
</video>

<p>That took about 2 minutes.  This is pretty promising especially because my desktop nVidia 3070 only has 8Gb dedicated memory so it’s not ‘giant’ (my new laptop has 12Gb, but let’s keep with Windows for the moment).</p>

<p>My next test will use Gemma4 in VS Code by way of Continue.dev’s plugin.  My plugin uses JSON, but yours might use YAML</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">  </span><span class="nl">"models"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"gemma4:e4b"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"provider"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ollama"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"model"</span><span class="p">:</span><span class="w"> </span><span class="s2">"gemma4:e4b"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="err">...</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span></code></pre></div></div>

<p><a href="/content/images/2026/04/gemma4-06.png"><img src="/content/images/2026/04/gemma4-06.png" alt="/content/images/2026/04/gemma4-06.png" /></a></p>

<p>I worked through making a service skill</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/gemma4-06.mp4" type="video/mp4" />
</video>

<p>Many times over, it would get stuck making files and i would nudge it along.  In the end, I just made the files for it and put in the contents.  While i spent roughly 30m on this, I was also distracted and reading some articles so it was more like 15m to get the the work done.</p>

<p>I wanted to see if any of the charts were a bit borked, so I did a local test:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ helm install --dry-run --debug myfakeskill ./helm-chart/
install.go:225: 2026-04-03 09:17:27.184010461 -0500 CDT m=+0.282573365 [debug] Original chart version: ""
install.go:242: 2026-04-03 09:17:27.184974141 -0500 CDT m=+0.283537045 [debug] CHART PATH: /home/builder/Workspaces/agentskills/python-app-setup/helm-chart

Error: INSTALLATION FAILED: template: fastapi-service/templates/secret.yaml:10:21: executing "fastapi-service/templates/secret.yaml" at &lt;.Values.secret.apiKeyValue&gt;: nil pointer evaluating interface {}.apiKeyValue
helm.go:86: 2026-04-03 09:17:27.265751891 -0500 CDT m=+0.364314795 [debug] template: fastapi-service/templates/secret.yaml:10:21: executing "fastapi-service/templates/secret.yaml" at &lt;.Values.secret.apiKeyValue&gt;: nil pointer evaluating interface {}.apiKeyValue
INSTALLATION FAILED
main.newInstallCmd.func2
        helm.sh/helm/v3/cmd/helm/install.go:158
github.com/spf13/cobra.(*Command).execute
        github.com/spf13/cobra@v1.8.1/command.go:985
github.com/spf13/cobra.(*Command).ExecuteC
        github.com/spf13/cobra@v1.8.1/command.go:1117
github.com/spf13/cobra.(*Command).Execute
        github.com/spf13/cobra@v1.8.1/command.go:1041
main.main
        helm.sh/helm/v3/cmd/helm/helm.go:85
runtime.main
        runtime/proc.go:283
runtime.goexit
        runtime/asm_amd64.s:1700
</code></pre></div></div>

<p>Let’s now try to fix this with Pi using our locally hosted models</p>

<p>I’ll create (or update) <code class="language-plaintext highlighter-rouge">~/.pi/agent/models.json</code></p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"providers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"ollama"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"baseUrl"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://192.168.1.160:11434/v1"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"api"</span><span class="p">:</span><span class="w"> </span><span class="s2">"openai-completions"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"apiKey"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ollama"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"models"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w"> </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"gemma4:e4b"</span><span class="w"> </span><span class="p">},</span><span class="w">
        </span><span class="p">{</span><span class="w"> </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"qwen3:latest"</span><span class="w"> </span><span class="p">}</span><span class="w">
      </span><span class="p">]</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Now, most will use <code class="language-plaintext highlighter-rouge">localhost</code> for the baseURL.  But I found WSL doesn’t “see” my windows hosted Ollama and if i use Ollama in WSL, it doesn’t properly see my nVidia card.  So even tho this is running in WSL on that exact host, I’m using my local IP seen in Windows (192.168.1.160) for the address.</p>

<p><a href="/content/images/2026/04/gemma4-07.png"><img src="/content/images/2026/04/gemma4-07.png" alt="/content/images/2026/04/gemma4-07.png" /></a></p>

<p>Let’s ask Pi (using Gemma4) to fix it</p>

<p><a href="/content/images/2026/04/gemma4-08.png"><img src="/content/images/2026/04/gemma4-08.png" alt="/content/images/2026/04/gemma4-08.png" /></a></p>

<p>It took about 2:40 to come up with it’s first idea for a fix, which required me to confirm (though Pi didn’t really suggest that, i just wrote “yes, do that” and hit enter).</p>

<p>However, over and over it would claim to do an edit but then just sit there. i ran that 3 times</p>

<p><a href="/content/images/2026/04/gemma4-09.png"><img src="/content/images/2026/04/gemma4-09.png" alt="/content/images/2026/04/gemma4-09.png" /></a></p>

<h2 id="opencode">OpenCode</h2>

<p>Like Pi, there is another local CLI we can use with Ollama, <a href="https://opencode.ai/">Opencode</a></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/agentskills$ npm i -g opencode-ai

added 5 packages in 8s
</code></pre></div></div>

<p>I need to create a local opencode json file with models</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/agentskills$ cat opencode.json
{
  "$schema": "https://opencode.ai/config.json",
  "provider": {
    "ollama": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "Ollama (local)",
      "options": {
        "baseURL": "http://192.168.1.160:11434/v1"
      },
      "models": {
        "gemma4:e4b": {
          "name": "gemma4"
        }
      }
    }
  }
}
</code></pre></div></div>

<p>It picked up the Anthropic key so I need to move to the gemma4 model I defined</p>

<p><a href="/content/images/2026/04/opencode-01.png"><img src="/content/images/2026/04/opencode-01.png" alt="/content/images/2026/04/opencode-01.png" /></a></p>

<p>I’ll feed the same prompt</p>

<p><a href="/content/images/2026/04/opencode-02.png"><img src="/content/images/2026/04/opencode-02.png" alt="/content/images/2026/04/opencode-02.png" /></a></p>

<p>This will kick in as it talks to Ollama in Windows</p>

<p><a href="/content/images/2026/04/opencode-03.png"><img src="/content/images/2026/04/opencode-03.png" alt="/content/images/2026/04/opencode-03.png" /></a></p>

<p>But it too failed</p>

<p><a href="/content/images/2026/04/opencode-04.png"><img src="/content/images/2026/04/opencode-04.png" alt="/content/images/2026/04/opencode-04.png" /></a></p>

<p>Here I feel like the guy with a Roomba who keeps setting it on top of the same piece of trash instead of just picking it up.</p>

<p>Fine, I’ll fix the damn error myself.</p>

<p>When we see:</p>

<blockquote>
  <p>Error: INSTALLATION FAILED: template: fastapi-service/templates/secret.yaml:10:26: executing “fastapi-service/templates/secret.yaml” at &lt;.Values.secret.apiKeyValue&gt;: nil pointer evaluating interface {}.apiKeyValue
helm.go:86: 2026-04-03 14:35:03.126362357 -0500 CDT m=+0.433510017 [debug] template: fastapi-service/templates/secret.yaml:10:26: executing “fastapi-service/templates/secret.yaml” at &lt;.Values.secret.apiKeyValue&gt;: nil pointer evaluating interface {}.apiKeyValue</p>
</blockquote>

<p>It is pretty clear that the secret should be wrapped in an if block and exposed in the values file.</p>

<p>We need to wrap it with an if, ie.g</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">{{</span><span class="nv">- if .Values.secret.enabled</span> <span class="pi">}}</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Secret</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">include "chart.fullname" .</span> <span class="pi">}}</span><span class="s">-secret</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="pi">{{</span><span class="nv">- include "chart.labels" . | nindent 2</span> <span class="pi">}}</span>
<span class="na">type</span><span class="pi">:</span> <span class="s">Opaque</span>
<span class="na">data</span><span class="pi">:</span>
  <span class="c1"># Example data key/value pair</span>
  <span class="na">api_key</span><span class="pi">:</span> <span class="pi">{{</span> <span class="nv">with .Values.secret.apiKeyValue</span> <span class="pi">}}{{</span> <span class="nv">. | b64enc</span> <span class="pi">}}{{</span> <span class="nv">end</span> <span class="pi">}}</span>
<span class="pi">{{</span><span class="nv">- end</span> <span class="pi">}}</span>
</code></pre></div></div>

<p>Then drop an enable (which is false by default) in the values</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>secret:
  enabled: false
  apiKeyValue: ""
</code></pre></div></div>

<p>I fixed the ingress and serviceaccount yamls and then it worked.</p>

<p><strong>Commentary:</strong> Those who say you can just vibe code and don’t need to know things are fooling themselves.</p>

<h1 id="using-the-skills-we-made-with-pi-and-gemma4">Using the Skills we made with Pi and Gemma4</h1>

<p>Again, just as before, you’ll see I have to nudge it along…</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/gemma4-10.mp4" type="video/mp4" />
</video>

<p>It made a start but really it just gave a start and couldn’t get much farther.</p>

<h2 id="gemini-cli">Gemini CLI</h2>

<p>I plan to just try Gemini CLI now to see if it can finish the job</p>

<p>I’ll give it the same prompt:</p>

<blockquote>
  <p>Create a containerized Perl app using the perl-app-setup skill.  The app should show a list of todos as stored locally in a local
datastore and let the user check off items and add new items.  they can also delete items.</p>
</blockquote>

<p>It very quickly wrapped up, mostly using the Gemini 2.5 flash and 3 flash models</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/gemma4-11.mp4" type="video/mp4" />
</video>

<h3 id="testing">Testing</h3>

<p>Let’s test the candidate work.</p>

<p>I’ll do a quick docker build</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/newApp$ docker build -t todo-app .
[+] Building 64.9s (17/17) FINISHED                                                                                       docker:default
 =&gt; [internal] load build definition from Dockerfile                                                                                0.0s
 =&gt; =&gt; transferring dockerfile: 801B                                                                                                0.0s
 =&gt; [internal] load metadata for docker.io/library/perl:5.38                                                                        1.3s
 =&gt; [internal] load metadata for docker.io/library/perl:5.38-slim                                                                   1.3s
 =&gt; [auth] library/perl:pull token for registry-1.docker.io                                                                         0.0s
 =&gt; [internal] load .dockerignore                                                                                                   0.0s
 =&gt; =&gt; transferring context: 158B                                                                                                   0.0s
 =&gt; [builder 1/6] FROM docker.io/library/perl:5.38@sha256:53060e111843f9b002cdea543dba7bfed54ff78e84b4abdf3b8df00451ea59c6         41.2s
 =&gt; =&gt; resolve docker.io/library/perl:5.38@sha256:53060e111843f9b002cdea543dba7bfed54ff78e84b4abdf3b8df00451ea59c6                  0.0s
 =&gt; =&gt; sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1 32B / 32B                                            0.3s
 =&gt; =&gt; sha256:47077a70fcc3f1fb0ef4638f82fd22d1b93ab6a3e98e64f66a157f4ce9ebedba 2.50kB / 2.50kB                                      0.0s
 =&gt; =&gt; sha256:53060e111843f9b002cdea543dba7bfed54ff78e84b4abdf3b8df00451ea59c6 9.01kB / 9.01kB                                      0.0s
 =&gt; =&gt; sha256:c663d63fb5298262de60d5e229e4b408d4c354795fe8019e9c8ccb6456a26613 5.83kB / 5.83kB                                      0.0s
 =&gt; =&gt; sha256:8f6ad858d0a46fa8ee628532c70b8dc82d06179d543b0b09ec19fc03d4c5b373 49.30MB / 49.30MB                                    4.1s
 =&gt; =&gt; sha256:b012eb15dff0bce418c03ec940325aee6aa4300d771c325728855697e620c63a 25.62MB / 25.62MB                                    3.4s
 =&gt; =&gt; sha256:ee3a0e7d77f0c84203cab438fcf345647c8121bbd80506a3c692f8608a14c4f4 67.78MB / 67.78MB                                    7.2s
 =&gt; =&gt; sha256:8688d0f2f567884eb217c6f80efa063bdb13a1951e92e6c5cac1ae5b736f5e1b 236.08MB / 236.08MB                                 12.9s
 =&gt; =&gt; sha256:6623c4016360f6a494137f7a0696634876fd3b5f84b642440e829aca640bd3b2 1.37kB / 1.37kB                                      4.3s
 =&gt; =&gt; extracting sha256:8f6ad858d0a46fa8ee628532c70b8dc82d06179d543b0b09ec19fc03d4c5b373                                           3.6s
 =&gt; =&gt; sha256:dd84d29e92517951df8c584ef70adac3b219a9e94e47073c3477b437fee3d6d3 15.56MB / 15.56MB                                    6.0s
 =&gt; =&gt; sha256:5c58bc0a198b5b32b37cf1fa1d2fc7bd96b5b9145df52faecab5106829908cff 132B / 132B                                          6.3s
 =&gt; =&gt; extracting sha256:b012eb15dff0bce418c03ec940325aee6aa4300d771c325728855697e620c63a                                           1.4s
 =&gt; =&gt; extracting sha256:ee3a0e7d77f0c84203cab438fcf345647c8121bbd80506a3c692f8608a14c4f4                                           5.3s
 =&gt; =&gt; extracting sha256:8688d0f2f567884eb217c6f80efa063bdb13a1951e92e6c5cac1ae5b736f5e1b                                          23.1s
 =&gt; =&gt; extracting sha256:6623c4016360f6a494137f7a0696634876fd3b5f84b642440e829aca640bd3b2                                           0.0s
 =&gt; =&gt; extracting sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1                                           0.0s
 =&gt; =&gt; extracting sha256:dd84d29e92517951df8c584ef70adac3b219a9e94e47073c3477b437fee3d6d3                                           2.4s
 =&gt; =&gt; extracting sha256:5c58bc0a198b5b32b37cf1fa1d2fc7bd96b5b9145df52faecab5106829908cff                                           0.0s
 =&gt; [internal] load build context                                                                                                   0.0s
 =&gt; =&gt; transferring context: 11.19kB                                                                                                0.0s
 =&gt; [runtime 1/4] FROM docker.io/library/perl:5.38-slim@sha256:877a0596aa8b5ef64dd7bde002fac9da4fbcebdc727927ee3cb36d520ad8a5dc     8.3s
 =&gt; =&gt; resolve docker.io/library/perl:5.38-slim@sha256:877a0596aa8b5ef64dd7bde002fac9da4fbcebdc727927ee3cb36d520ad8a5dc             0.0s
 =&gt; =&gt; sha256:c3507f6b13e970e15b0a7f80894ebaf5585dcc16d37c13332bcd4ace74cf9de1 1.37kB / 1.37kB                                      0.4s
 =&gt; =&gt; sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1 32B / 32B                                            0.3s
 =&gt; =&gt; sha256:877a0596aa8b5ef64dd7bde002fac9da4fbcebdc727927ee3cb36d520ad8a5dc 9.02kB / 9.02kB                                      0.0s
 =&gt; =&gt; sha256:b82b5ea7daa1a0755078a5297c8ecdf26e7ae43e8e759a372e1df8d0f7570dc9 1.92kB / 1.92kB                                      0.0s
 =&gt; =&gt; sha256:77f07f06f0362b7b34dcaed39280b3ea0f8ec3b0d21492b86b5071e2fa5a7b69 4.75kB / 4.75kB                                      0.0s
 =&gt; =&gt; sha256:ec781dee3f4719c2ca0dd9e73cb1d4ed834ed1a406495eb6e44b6dfaad5d1f8f 29.78MB / 29.78MB                                    1.5s
 =&gt; =&gt; sha256:444f25e108e264b00dae5fb350a4fbac78784586273ef37f290bdd3c8889599f 31.60MB / 31.60MB                                    2.9s
 =&gt; =&gt; sha256:e1debe2bad764c01de94cf2890688af6b6128e923bca730b5814a2696dd457f5 132B / 132B                                          0.7s
 =&gt; =&gt; extracting sha256:ec781dee3f4719c2ca0dd9e73cb1d4ed834ed1a406495eb6e44b6dfaad5d1f8f                                           2.3s
 =&gt; =&gt; extracting sha256:c3507f6b13e970e15b0a7f80894ebaf5585dcc16d37c13332bcd4ace74cf9de1                                           0.0s
 =&gt; =&gt; extracting sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1                                           0.0s
 =&gt; =&gt; extracting sha256:444f25e108e264b00dae5fb350a4fbac78784586273ef37f290bdd3c8889599f                                           3.8s
 =&gt; =&gt; extracting sha256:e1debe2bad764c01de94cf2890688af6b6128e923bca730b5814a2696dd457f5                                           0.0s
 =&gt; [runtime 2/4] WORKDIR /app                                                                                                      1.3s
 =&gt; [builder 2/6] WORKDIR /app                                                                                                      1.8s
 =&gt; [builder 3/6] RUN cpan App::cpanminus &amp;&amp;     apt-get update &amp;&amp; apt-get install -y --no-install-recommends     build-essential  11.8s
 =&gt; [builder 4/6] COPY cpanfile .                                                                                                   0.0s
 =&gt; [builder 5/6] RUN cpanm --installdeps --notest .                                                                                6.3s
 =&gt; [builder 6/6] COPY . .                                                                                                          0.0s
 =&gt; [runtime 3/4] COPY --from=builder /usr/local/lib/perl5 /usr/local/lib/perl5                                                     0.7s
 =&gt; [runtime 4/4] COPY --from=builder /app .                                                                                        0.0s
 =&gt; exporting to image                                                                                                              0.6s
 =&gt; =&gt; exporting layers                                                                                                             0.5s
 =&gt; =&gt; writing image sha256:91c275518148c8e09222cd3c9c1f3db6719bd4105156aabd3cd11035e538f29c                                        0.0s
 =&gt; =&gt; naming to docker.io/library/todo-app                                                                                         0.0s
</code></pre></div></div>

<p>Running it shows this is just a command line app</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/newApp$ docker run -it --rm -v $(pwd)/data:/app/data todo-app -l
No todos found.
builder@DESKTOP-QADGF36:~/Workspaces/newApp$ docker run -it --rm -v $(pwd)/data:/app/data todo-app
No todos found.
</code></pre></div></div>

<p>I suppose I never demanded it be web-based.  I can just add a task and see it listed</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/newApp$ perl bin/app.pl --add "Complete the coding challenge"
Added todo: Complete the coding challenge (ID: 1)
builder@DESKTOP-QADGF36:~/Workspaces/newApp$ docker run -it --rm -v $(pwd)/data:/app/data todo-app
ID  | Status     | Description
----------------------------------------
1   | [ ]        | Complete the coding challenge
</code></pre></div></div>

<h3 id="testing-python-app-skill">Testing Python app skill</h3>

<p>Let’s now test the Kubernetes service Python app skill</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ln -s /home/builder/Workspaces/agentskills/python-app-setup /home/builder/.agents/skills/python-app-setup
</code></pre></div></div>

<p>I’m going to set a higher autocomplete and set steering and followup to all</p>

<p><a href="/content/images/2026/04/pi-10.png"><img src="/content/images/2026/04/pi-10.png" alt="/content/images/2026/04/pi-10.png" /></a></p>

<p>I’m going to pick the skill to sort of prime the pump on it</p>

<p><a href="/content/images/2026/04/pi-11.png"><img src="/content/images/2026/04/pi-11.png" alt="/content/images/2026/04/pi-11.png" /></a></p>

<p>My hope is that with Python, it might move faster than with Perl (which is arguably older and less common).</p>

<p>But time and time again, it failed to actually dot he work it planned</p>

<p><a href="/content/images/2026/04/pi-12.png"><img src="/content/images/2026/04/pi-12.png" alt="/content/images/2026/04/pi-12.png" /></a></p>

<p>However, when I moved to my Linux laptop and ran the same prompt:</p>

<blockquote>
  <p>Create a new project based on the guidelines from the skill my-python-app-skill.  It should be a Todo app that is exposed via
kubernetes and has a web interface.  do not confirm each step, just proceed</p>
</blockquote>

<p>It went way faster and had no issues writing files</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/opencode-05.mp4" type="video/mp4" />
</video>

<p>I decided to test again using Gemma via Continue.dev.</p>

<p>This time I would use the larger video card in the gaming laptop (which is on 192.168.1.220), but with my local windows VS Code</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"models"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"gemma4:e4b"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"provider"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ollama"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"model"</span><span class="p">:</span><span class="w"> </span><span class="s2">"gemma4:e4b"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"model"</span><span class="p">:</span><span class="w"> </span><span class="s2">"gemma4:e4b"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"gemma4:e4b (Legion)"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"completionOptions"</span><span class="p">:</span><span class="w"> </span><span class="p">{},</span><span class="w">
      </span><span class="nl">"apiBase"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://192.168.1.220:11434"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"provider"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ollama"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"model"</span><span class="p">:</span><span class="w"> </span><span class="s2">"gemma4:26b"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"gemma4:26b (Legion)"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"completionOptions"</span><span class="p">:</span><span class="w"> </span><span class="p">{},</span><span class="w">
      </span><span class="nl">"apiBase"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://192.168.1.220:11434"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"provider"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ollama"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="err">...</span><span class="w">
</span></code></pre></div></div>

<p>I could get it to list files, but not read them when trying to create an Ansible index:</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/opencode-06.mp4" type="video/mp4" />
</video>

<p>using the <code class="language-plaintext highlighter-rouge">@files</code> just sends it to lunch and it never returns.</p>

<p><a href="/content/images/2026/04/gemma4-12.png"><img src="/content/images/2026/04/gemma4-12.png" alt="/content/images/2026/04/gemma4-12.png" /></a></p>

<p>I pivoted to Pi in WSL, but using the more powerful laptop (updating the ~/.pi/agent/models.json)</p>

<p><a href="/content/images/2026/04/pi-13.png"><img src="/content/images/2026/04/pi-13.png" alt="/content/images/2026/04/pi-13.png" /></a></p>

<p>I used <code class="language-plaintext highlighter-rouge">.</code> to try and tell Gemma4 to ‘move it along’</p>

<p><a href="/content/images/2026/04/pi-14.png"><img src="/content/images/2026/04/pi-14.png" alt="/content/images/2026/04/pi-14.png" /></a></p>

<p>But it didn’t update the index</p>

<p><a href="/content/images/2026/04/pi-15.png"><img src="/content/images/2026/04/pi-15.png" alt="/content/images/2026/04/pi-15.png" /></a></p>

<p>I tried several rounds with qwen3.5 as well, being very explicit in my ask - it too failed</p>

<p><a href="/content/images/2026/04/pi-16.png"><img src="/content/images/2026/04/pi-16.png" alt="/content/images/2026/04/pi-16.png" /></a></p>

<p>I even tried <a href="https://github.com/tcsenpai/ollama-code">Ollama code</a></p>

<video muted="" controls="">
    <source src="/content/images/2026/04/ollamacode-01.mp4" type="video/mp4" />
</video>

<p>Is my ask too hard? I needed to sanity check it against Gemini CLI</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/geminicli-01.mp4" type="video/mp4" />
</video>

<p>This just used the flash model and knocked it out.</p>

<p><a href="/content/images/2026/04/pi-17.png"><img src="/content/images/2026/04/pi-17.png" alt="/content/images/2026/04/pi-17.png" /></a></p>

<h2 id="research">Research</h2>

<p>Let’s put a pin in GenAI coding for now.  How about just general research?</p>

<p><a href="/content/images/2026/04/ollamacode-02.png"><img src="/content/images/2026/04/ollamacode-02.png" alt="/content/images/2026/04/ollamacode-02.png" /></a></p>

<p>It made a table, but it’s not readable - likely an Ollama Code issue more than a model issue</p>

<p><a href="/content/images/2026/04/ollamacode-03.png"><img src="/content/images/2026/04/ollamacode-03.png" alt="/content/images/2026/04/ollamacode-03.png" /></a></p>

<p>It immediately forgot it’s last request, so i just rephrased without asking for a table and got a good start for an article</p>

<p><a href="/content/images/2026/04/ollamacode-04.png"><img src="/content/images/2026/04/ollamacode-04.png" alt="/content/images/2026/04/ollamacode-04.png" /></a></p>

<p>I’m not a finance guy, so I wouldn’t really write a piece like that, but I might ask for some examples to punch up a topic I <em>am</em> writing about:</p>

<p>This could be a windows issue, but for the life of me I couldn’t get Ollama code to stop truncating the tables.  I did, however, figure out a workaround - just use a ridiculous large terminal window</p>

<p><a href="/content/images/2026/04/ollamacode-05.png"><img src="/content/images/2026/04/ollamacode-05.png" alt="/content/images/2026/04/ollamacode-05.png" /></a></p>

<h1 id="summary">Summary</h1>

<p>The first part of this article covered a user request to review Anthropics Claude Code versus using the same models in Pi.</p>

<p>The behavior of Claude code was to kick out up to 6 agents to do things, then come back and another 6 agents to evaluate and judge the work.  The output in the end was fine, but it also wasn’t particularly fast.  I found the time I spent building out the helm chart skill with Claude Code was about the same as it was building perl app skill.   The quality <em>might</em> be better with helm but at what price?</p>

<p>I spent about US$0.35 making the Perl skill and about US$2.30 with Claude Code making the helm chart one.  Both used the Anthropic “skill builder” skill.  Assuming the quality is similar, I might use Pi from here on out.  However, as a “free tier” user that pays per token, the money factor might really be my underlying driver.</p>

<p>Next we looked at Gemma4, which Google dropped just last week.  It’s quite thorough and gave good answers.  I tested it in VS Code with Continue.dev, Opencode, Ollama-code and Pi.  I used Windows and Linux.  Overall, I had better success with Pi and Opencode in Linux than anything else.</p>

<p>However, for coding work, Gemma4 is so damn timid.  It just wants to check constantly back with the user.  When you are using lightweight coding agents without much memory, this becomes a real issue as it just keeps forgetting what it was just asking you about and starts back over.  I had really limited success with all my approaches.  I just wanted to tell Gemma4 “just do the damn thing and don’t ask me!” but regardless of how I would phrase things, it would quickly forget and fail to write files.</p>

<p>To sanity check, I would pivot over to a Github free version of Copilot, or using Gemini CLI with Gemini 3 Flash/Flash lite and each time they would complete the work.</p>

<p>I will own that this could be a user error, some kind of “me problem” that needs more investigation.</p>

<p>When I pivoted to using Gemma4 for ideaation it did fantastic.  While I’m not planning to write blog posts on refinancing, using it to come up with ideas for MCP vs Skills is something I might do.   The use case here for such things becomes apparent when you are in the life phase with kids in sports.</p>

<p>Often I’ll be killing time in the backfield of Softball practice or in a swimming pool area built like a Faraday cage and just unable to get signal.  Having an LLM option to help come up with ideas is really useful.</p>

<p>My common case is I’ll be writing a blog, or updating a deck for a presentation and find one area is kind of light on examples.  I’ll ask the LLM something like “provide 5 common ways to migrate a vSphere hosted windows VM to GCP Compute Engine” and then of the 5 returned, 3 might sound plausible to me and get me going again.  It can help provide that spark when my mental matches are wet.</p>]]></content><author><name>Isaac Johnson</name></author><category term="genai" /><category term="claude" /><category term="anthropic" /><category term="gemma" /><category term="opencode" /><category term="pi" /><category term="continue.dev" /><summary type="html"><![CDATA[I had two things I wanted to tackle today: Comparing using Anthropic via Pi CLI versus Claude Code, then taking a look at Gemma4 - the new open model Google released last week. In this article, I want to dig into Gemma4 more thoroughly rather than rush an article just to get on the headline bandwagon.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://freshbrewed.science/content/images/2026/04/ollamapiegb.png" /><media:content medium="image" url="https://freshbrewed.science/content/images/2026/04/ollamapiegb.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">AI Video in 2026</title><link href="https://freshbrewed.science/2026/04/07/aivid.html" rel="alternate" type="text/html" title="AI Video in 2026" /><published>2026-04-07T10:00:01+00:00</published><updated>2026-04-07T10:00:01+00:00</updated><id>https://freshbrewed.science/2026/04/07/aivid</id><content type="html" xml:base="https://freshbrewed.science/2026/04/07/aivid.html"><![CDATA[<p>A user recently requested by way of the form at the top that I look into <a href="https://kubrix.co/">Kubrix AI</a>.  I wasn’t sure if it the person was involved with the project or genuine interest.  Either way, I warned them I would do an honest take.</p>

<p>While I’m at it, I’ll try a bunch of the prior AI video generation sites I’ve used (Kling, Pollo) as well as my tried and true Gemini and Midjourney.  I’ll also see what I can find that is new (and most important “free”) while I’m at it.</p>

<h1 id="kubrix">Kubrix</h1>

<p>First, the splash screen makes me want to vomit.</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/kubrix-01.mp4" type="video/mp4" />
</video>

<p>I gave it a prompt and then picked a size and duration</p>

<p><a href="/content/images/2026/04/kubrix-02.png"><img src="/content/images/2026/04/kubrix-02.png" alt="/content/images/2026/04/kubrix-02.png" /></a></p>

<p>I used Analog File for a style</p>

<p><a href="/content/images/2026/04/kubrix-03.png"><img src="/content/images/2026/04/kubrix-03.png" alt="/content/images/2026/04/kubrix-03.png" /></a></p>

<p>lighting style</p>

<p><a href="/content/images/2026/04/kubrix-04.png"><img src="/content/images/2026/04/kubrix-04.png" alt="/content/images/2026/04/kubrix-04.png" /></a></p>

<p>A frame</p>

<p><a href="/content/images/2026/04/kubrix-05.png"><img src="/content/images/2026/04/kubrix-05.png" alt="/content/images/2026/04/kubrix-05.png" /></a></p>

<p>Then a model</p>

<p><a href="/content/images/2026/04/kubrix-06.png"><img src="/content/images/2026/04/kubrix-06.png" alt="/content/images/2026/04/kubrix-06.png" /></a></p>

<p>and an optional start image</p>

<p><a href="/content/images/2026/04/kubrix-07.png"><img src="/content/images/2026/04/kubrix-07.png" alt="/content/images/2026/04/kubrix-07.png" /></a></p>

<p>Lastly, a Generate page that says this 4-second video will be “1800 credits”</p>

<p><a href="/content/images/2026/04/kubrix-08.png"><img src="/content/images/2026/04/kubrix-08.png" alt="/content/images/2026/04/kubrix-08.png" /></a></p>

<p>It showed it would use the prompt:</p>

<blockquote>
  <p>A cinematic 4-second opening sequence for a modern ALF reboot. The shot begins with a rapid, smooth dolly-in across a sleek, contemporary suburban living room bathed in the amber glow of a California sunset and accented by cool teal and magenta smart-lighting. ALF, the iconic alien from Melmac, is perched on a minimalist charcoal velvet sofa, looking more realistic than ever with high-fidelity, hand-groomed brown fur textures and expressive, glossy eyes that reflect the room’s ambient light. As the camera zooms in, ALF nonchalantly tosses a high-tech tablet aside, turns toward the lens, and breaks the fourth wall with a signature mischievous smirk and a quick, knowing wink. The atmosphere is vibrant and polished, featuring professional 8K resolution, shallow depth of field that blurs the modern kitchen in the background, and crisp, cinematic color grading. In the final second, a sleek, chrome-textured “ALF” logo with a subtle digital glitch effect emerges in the foreground, pulsing with retro-wave energy.</p>
</blockquote>

<p>I signed in next and see as a new user I get 100 credits and the new price is now only 1000 credits</p>

<p><a href="/content/images/2026/04/kubrix-09.png"><img src="/content/images/2026/04/kubrix-09.png" alt="/content/images/2026/04/kubrix-09.png" /></a></p>

<p>This leads me to “how to get credits”.</p>

<p>I can either subscribe to a monthly fee, the lowest of which is US$30</p>

<p><a href="/content/images/2026/04/kubrix-10.png"><img src="/content/images/2026/04/kubrix-10.png" alt="/content/images/2026/04/kubrix-10.png" /></a></p>

<p>or a one-time top up for $40</p>

<p><a href="/content/images/2026/04/kubrix-11.png"><img src="/content/images/2026/04/kubrix-11.png" alt="/content/images/2026/04/kubrix-11.png" /></a></p>

<p>I was willing to deal with at most US$10 to try a thing so unfortunately we will have to pass.</p>

<h1 id="midjourney">Midjourney</h1>

<p>I was curious what Midjourney would do with that prompt.  Even though I mostly use Gemini now when I need a bit of AI graphics, I still pay for MJ as it was the first that really offered solid AI Generated art.</p>

<p>As you can see, it’s generally best to get some images then run a video from the best of them (if there is a best of them)</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/midjourney-01.mp4" type="video/mp4" />
</video>

<p>I tried the same prompt, but this time fed it an ALF still to start</p>

<p>The text is a mess, but I was surprised how it accurately figured out how Shumway talked just from a still</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/midjourney-02.mp4" type="video/mp4" />
</video>

<p>and a regen</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/midjourney-03.mp4" type="video/mp4" />
</video>

<h1 id="gemini-veo2-video">Gemini (VEO2) video</h1>

<p>Let’s put the same thing in Gemini to see what it comes up with</p>

<video muted="" controls="">
    <source src="/content/images/2026/04/gemini-01.mp4" type="video/mp4" />
</video>

<p>Here is the video (with some strange sound effects)</p>

<video controls="">
    <source src="/content/images/2026/04/gemini-02.mp4" type="video/mp4" />
</video>

<p>I gave it an image to refine the video</p>

<p><a href="/content/images/2026/04/gemini-03.png"><img src="/content/images/2026/04/gemini-03.png" alt="/content/images/2026/04/gemini-03.png" /></a></p>

<p>which did better visually</p>

<video controls="">
    <source src="/content/images/2026/04/gemini-04.mp4" type="video/mp4" />
</video>

<p>One last refinement. I asked “Can you give him a deeper New York snarky comedic voice”?</p>

<video controls="">
    <source src="/content/images/2026/04/gemini-05.mp4" type="video/mp4" />
</video>

<p>I tried again the next day…</p>

<video controls="">
    <source src="/content/images/2026/04/gemini-06.mp4" type="video/mp4" />
</video>

<p>When I asked it to tweak the voice for a NJ accent, it re-imagined the look again</p>

<video controls="">
    <source src="/content/images/2026/04/gemini-07.mp4" type="video/mp4" />
</video>

<p>This highlights one small limit.  Using Google AI Pro (which I got a year of with my last Google Pixel Fold phone), I get 3 videos a day.  So to do more, I either pay out of pocket in AI Studio in GCP or I just wait till tomorrow.</p>

<h1 id="kling">Kling</h1>

<p>Next I gave Kling a try</p>

<p><a href="/content/images/2026/04/kling-01.png"><img src="/content/images/2026/04/kling-01.png" alt="/content/images/2026/04/kling-01.png" /></a></p>

<p>But even though I have sufficient credits, it would seem they paused the free tier</p>

<p><a href="/content/images/2026/04/kling-02.png"><img src="/content/images/2026/04/kling-02.png" alt="/content/images/2026/04/kling-02.png" /></a></p>

<p>I came back a couple days in a row just to check, but it was still blocking free tier.</p>

<h1 id="self-comfyui">Self (ComfyUI)</h1>

<p>As we’ve shown, using something like ComfyUI on a computer with a GPU, one can just create these kind of things locally.</p>

<p>I fired up ComfyUI.</p>

<p>I’m going to use a Wan Vace 14b template I have bookmarked.</p>

<p>It might looking daunting, but really I just need to give a seed image and a prompt (see the arrows)</p>

<p><a href="/content/images/2026/04/comfyui-01.png"><img src="/content/images/2026/04/comfyui-01.png" alt="/content/images/2026/04/comfyui-01.png" /></a></p>

<p>I’ve translated those Chinese statements in the negative block before and they are pretty canned “no watermark, no junk, etc…”.</p>

<video controls="">
    <source src="/content/images/2026/04/ComfyUI_00006_.mp4" type="video/mp4" />
</video>

<p>I then tried with LTX2 (needed a 40Gb model downloaded)</p>

<p><a href="/content/images/2026/04/comfyui-02.png"><img src="/content/images/2026/04/comfyui-02.png" alt="/content/images/2026/04/comfyui-02.png" /></a></p>

<p>a little weird but could be a start</p>

<video controls="">
    <source src="/content/images/2026/04/LTX-2_00016_.mp4" type="video/mp4" />
</video>

<p>I tried LTX 2.3 which can take a video input with the same prompt and ALF still</p>

<p><a href="/content/images/2026/04/comfyui-03.png"><img src="/content/images/2026/04/comfyui-03.png" alt="/content/images/2026/04/comfyui-03.png" /></a></p>

<p>Though it gave me the oddest output</p>

<video controls="">
    <source src="/content/images/2026/04/LTX_2.3_t2v_00001_.mp4" type="video/mp4" />
</video>

<p>A second pass with just asking for a “The TV intro for an ALF sitcom reboot.” (but also with the still)</p>

<video controls="">
    <source src="/content/images/2026/04/LTX_2.3_t2v_00002_.mp4" type="video/mp4" />
</video>

<p>One last go, I set the prompt to “The TV intro for an ALF sitcom reboot.  Should include this character”</p>

<video controls="">
    <source src="/content/images/2026/04/LTX_2.3_t2v_00003_.mp4" type="video/mp4" />
</video>

<h1 id="seevideoai">Seevideo.ai</h1>

<p>I found an <a href="https://seevideo.ai/">AI Video site, seevideo.ai</a> that would give me just enough first-time credits (100) to try Seedance 1.5.  It isn’t the latest 2.0, but it was worth a try</p>

<p><a href="/content/images/2026/04/seevideoai-01.png"><img src="/content/images/2026/04/seevideoai-01.png" alt="/content/images/2026/04/seevideoai-01.png" /></a></p>

<video controls="">
    <source src="/content/images/2026/04/seevideoai-01.mp4" type="video/mp4" />
</video>

<h1 id="dreamina-capcut">Dreamina capcut</h1>

<p>Another one that gave me just enough for Seedance 1.5 Pro was <a href="https://dreamina.capcut.com/">Dreamina</a></p>

<p><a href="/content/images/2026/04/dreamina-01.png"><img src="/content/images/2026/04/dreamina-01.png" alt="/content/images/2026/04/dreamina-01.png" /></a></p>

<video controls="">
    <source src="/content/images/2026/04/dreamina-01.mp4" type="video/mp4" />
</video>

<h1 id="pollo-ai">Pollo AI</h1>

<p><a href="https://pollo.ai/">Pollo.ai</a> has it’s own model as well as others.  I’ve used them in the past for some nice intro videos.</p>

<p><a href="/content/images/2026/04/pollo-01.png"><img src="/content/images/2026/04/pollo-01.png" alt="/content/images/2026/04/pollo-01.png" /></a></p>

<video controls="">
    <source src="/content/images/2026/04/pollo-01.mp4" type="video/mp4" />
</video>

<p>It was interesting how it showed the still but then just did it’s own thing</p>

<h1 id="seedance2ai">Seedance2ai</h1>

<p>One that wouldn’t let me use Seedance2 (for pro customers), but did let me try the now defunct Sora2 was <a href="https://www.seedance2ai.io/">Seedance2ai</a></p>

<p><a href="/content/images/2026/04/seedance2ai-01.png"><img src="/content/images/2026/04/seedance2ai-01.png" alt="/content/images/2026/04/seedance2ai-01.png" /></a></p>

<p>But I have a feeling it will never complete</p>

<p><a href="/content/images/2026/04/seedance2ai-02.png"><img src="/content/images/2026/04/seedance2ai-02.png" alt="/content/images/2026/04/seedance2ai-02.png" /></a></p>

<p>In the end my generated workspace disappeared (but didn’t charge the credits)</p>

<h1 id="summary">Summary</h1>

<p>We tried many many AI video generators today.  It is not that surprising that we can find an endless assortment of new AI startups that will give just enough credits to try a video or two so I question those video generation sites that give new users nothing.  There is no real motivation.</p>

<p>This post started with a user request to review <a href="https://kubrix.co/">Kubrix AI</a>.  Since they offer me nothing and have this awful background video, I can say confidently I won’t be returning.  Of the new ones, the closest I got to paying was <a href="https://www.seedance2ai.io/">Seedance2ai.io</a> as if I dropped the coin for an annual plan, I could have used Seedance2.  That said, <a href="https://soro2.ai/">soro2.ai</a> would let me if I dropped a one-time $50 top up.</p>

<p>I could have used PowerDirector as I have 100 credits in there as an annual subscriber. but they didn’t offer any unique models I wanted to try</p>

<p><a href="/content/images/2026/04/powerdirector-01.png"><img src="/content/images/2026/04/powerdirector-01.png" alt="/content/images/2026/04/powerdirector-01.png" /></a></p>

<p>If I was tasked on making a TV intro with just the tools at hand, I would likely splice some MJ clips together and use <a href="https://suno.com/">Suno</a> to make a Jingle…</p>

<p><a href="/content/images/2026/04/suno-01.png"><img src="/content/images/2026/04/suno-01.png" alt="/content/images/2026/04/suno-01.png" /></a></p>

<p>then create with a style</p>

<p><a href="/content/images/2026/04/suno-02.png"><img src="/content/images/2026/04/suno-02.png" alt="/content/images/2026/04/suno-02.png" /></a></p>

<p>Something like</p>

<video controls="">
    <source src="/content/images/2026/04/ALFJingle.mp4" type="video/mp4" />
</video>]]></content><author><name>Isaac Johnson</name></author><category term="GenAI" /><category term="kling" /><category term="pollo" /><category term="seedance" /><category term="gemini" /><category term="ComfyUI" /><category term="midjourney" /><summary type="html"><![CDATA[A user recently requested by way of the form at the top that I look into Kubrix AI. I wasn’t sure if it the person was involved with the project or genuine interest. Either way, I warned them I would do an honest take.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://freshbrewed.science/content/images/2026/04/alfstill.png" /><media:content medium="image" url="https://freshbrewed.science/content/images/2026/04/alfstill.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">OS Apps: Dynacat and Seafile</title><link href="https://freshbrewed.science/2026/04/02/osapps.html" rel="alternate" type="text/html" title="OS Apps: Dynacat and Seafile" /><published>2026-04-02T01:01:01+00:00</published><updated>2026-04-02T01:01:01+00:00</updated><id>https://freshbrewed.science/2026/04/02/osapps</id><content type="html" xml:base="https://freshbrewed.science/2026/04/02/osapps.html"><![CDATA[<p>A while back I saw this <a href="https://mariushosting.com/how-to-install-dynacat-on-your-synology-nas/">Marius post</a> about <a href="https://github.com/Panonim/dynacat">Dynacat</a>, an interesting fork of Glances that looks to be more self-contained.</p>

<p>Also from <a href="https://mariushosting.com/how-to-install-seafile-13-on-your-synology-nas/">a Marius post</a> I noted <a href="https://manual.seafile.com/latest/">Seafile</a>.  Seafile looks to be a much more complete file sharing and wiki suite which can run in Docker.  We’ll give that a shot as well.</p>

<h1 id="dynacat-install">Dynacat install</h1>

<p>We’ll find the install steps on the Github page</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mkdir dynacat &amp;&amp; cd dynacat &amp;&amp; \
curl -sL https://github.com/glanceapp/docker-compose-template/archive/refs/heads/main.tar.gz | tar -xzf - --strip-components 2 &amp;&amp; \
sed -i \
  -e 's/^  glance:/  dynacat:/' \
  -e 's/^    container_name: glance/    container_name: dynacat/' \
  -e 's/^    image: glanceapp\/glance/    image: panonim\/dynacat/' \
  docker-compose.yml &amp;&amp; \
mv config/glance.yml config/dynacat.yml
</code></pre></div></div>

<p>Let’s execute that</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/dynacat$ mkdir dynacat &amp;&amp; cd dynacat &amp;&amp; \
curl -sL https://github.com/glanceapp/docker-compose-template/archive/refs/heads/main.tar.gz | tar -xzf - --strip-components 2 &amp;&amp; \
sed -i \
  -e 's/^  glance:/  dynacat:/' \
  -e 's/^    container_name: glance/    container_name: dynacat/' \
  -e 's/^    image: glanceapp\/glance/    image: panonim\/dynacat/' \
  docker-compose.yml &amp;&amp; \
mv config/glance.yml config/dynacat.yml
builder@DESKTOP-QADGF36:~/Workspaces/dynacat/dynacat$ cat config/dynacat.yml
server:
  assets-path: /app/assets

theme:
  # Note: assets are cached by the browser, changes to the CSS file
  # will not be reflected until the browser cache is cleared (Ctrl+F5)
  custom-css-file: /assets/user.css

pages:
  # It's not necessary to create a new file for each page and include it, you can simply
  # put its contents here, though multiple pages are easier to manage when separated
  - $include: home.yml
builder@DESKTOP-QADGF36:~/Workspaces/dynacat/dynacat$ cat docker-compose.yml
services:
  dynacat:
    container_name: dynacat
    image: panonim/dynacat
    restart: unless-stopped
    volumes:
      - ./config:/app/config
      - ./assets:/app/assets
      - /etc/localtime:/etc/localtime:ro
      # Optionally, also mount docker socket if you want to use the docker containers widget
      # - /var/run/docker.sock:/var/run/docker.sock:ro
    ports:
      - 8080:8080
    env_file: .env
</code></pre></div></div>

<p>Now we can launch Dynacat with Docker compose up</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/dynacat/dynacat$ docker compose up
[+] Running 5/5
 ✔ dynacat Pulled                                                                                                       3.4s
   ✔ bc1da058f299 Pull complete                                                                                         1.2s
   ✔ 5ab4e787c3f9 Pull complete                                                                                         1.2s
   ✔ e7c021652cab Pull complete                                                                                         2.0s
   ✔ 7df5d8721fac Pull complete                                                                                         2.1s
[+] Running 2/2
 ✔ Network dynacat_default  Created                                                                                     0.1s
 ✔ Container dynacat        Created                                                                                     0.2s
Attaching to dynacat
dynacat  | 2026/03/28 16:33:15 Starting server on :8080 (base-url: "", assets-path: "/app/assets")
</code></pre></div></div>

<p>We can see it fired up on 8080</p>

<p><a href="/content/images/2026/03/dynacat-01.png"><img src="/content/images/2026/03/dynacat-01.png" alt="/content/images/2026/03/dynacat-01.png" /></a></p>

<p>Let’s take a look at the config</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cat config/home.yml
- name: Home
  # Optionally, if you only have a single page you can hide the desktop navigation for a cleaner look
  # hide-desktop-navigation: true
  columns:
    - size: small
      widgets:
        - type: calendar
          first-day-of-week: sunday

        - type: rss
          limit: 10
          collapse-after: 3
          cache: 12h
          feeds:
            - url: https://selfh.st/rss/
              title: selfh.st
            - url: https://ciechanow.ski/atom.xml
            - url: https://www.joshwcomeau.com/rss.xml
              title: Josh Comeau
            - url: https://freshbrewed.science/feed.xml
              title: Fresh Brewed
            - url: https://ishadeed.com/feed.xml
              title: Ahmad Shadeed

        - type: twitch-channels
          channels:
            - theprimeagen
            - j_blow
            - giantwaffle
            - cohhcarnage
            - christitustech
            - EJ_SA

    - size: full
      widgets:
        - type: group
          widgets:
            - type: hacker-news
            - type: lobsters

        - type: videos
          channels:
            - UCXuqSBlHAE6Xw-yeJA0Tunw # Linus Tech Tips
            - UCR-DXc1voovS8nhAvccRZhg # Jeff Geerling
            - UCsBjURrPoezykLs9EqgamOA # Fireship
            - UCBJycsmduvYEL83R_U4JriQ # Marques Brownlee
            - UCi8C7TNs2ohrc6hnRQ5Sn2w # Kai Lentit

        - type: group
          widgets:
            - type: reddit
              subreddit: technology
              show-thumbnails: true
            - type: reddit
              subreddit: selfhosted
              show-thumbnails: true

    - size: small
      widgets:
        - type: weather
          location: Woodbury, Minnesota
          units: imperial # alternatively "metric"
          hour-format: 12h # alternatively "24h"
          # Optionally hide the location from being displayed in the widget
          # hide-location: true

        - type: markets
          markets:
            - symbol: SPY
              name: S&amp;P 500
            - symbol: BTC-USD
              name: Bitcoin
            - symbol: NVDA
              name: NVIDIA
            - symbol: AAPL
              name: Apple
            - symbol: MSFT
              name: Microsoft

        - type: releases
          cache: 1d
          # Without authentication the Github API allows for up to 60 requests per hour. You can create a
          # read-only token from your Github account settings and use it here to increase the limit.
          # token: ...
          repositories:
            - glanceapp/glance
            - go-gitea/gitea
            - immich-app/immich
            - syncthing/syncthing
</code></pre></div></div>

<p>I had to tweak the config a bit to get weather to work, but my final working one was</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">$ cat config/home.yml</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Home</span>
  <span class="c1"># Optionally, if you only have a single page you can hide the desktop navigation for a cleaner look</span>
  <span class="c1"># hide-desktop-navigation: true</span>
  <span class="na">columns</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">size</span><span class="pi">:</span> <span class="s">small</span>
      <span class="na">widgets</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">calendar</span>
          <span class="na">first-day-of-week</span><span class="pi">:</span> <span class="s">sunday</span>

        <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">rss</span>
          <span class="na">limit</span><span class="pi">:</span> <span class="m">10</span>
          <span class="na">collapse-after</span><span class="pi">:</span> <span class="m">3</span>
          <span class="na">cache</span><span class="pi">:</span> <span class="s">12h</span>
          <span class="na">feeds</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">url</span><span class="pi">:</span> <span class="s">https://selfh.st/rss/</span>
              <span class="na">title</span><span class="pi">:</span> <span class="s">selfh.st</span>
            <span class="pi">-</span> <span class="na">url</span><span class="pi">:</span> <span class="s">https://ciechanow.ski/atom.xml</span>
            <span class="pi">-</span> <span class="na">url</span><span class="pi">:</span> <span class="s">https://www.joshwcomeau.com/rss.xml</span>
              <span class="na">title</span><span class="pi">:</span> <span class="s">Josh Comeau</span>
            <span class="pi">-</span> <span class="na">url</span><span class="pi">:</span> <span class="s">https://freshbrewed.science/feed.xml</span>
            <span class="pi">-</span> <span class="na">url</span><span class="pi">:</span> <span class="s">https://ishadeed.com/feed.xml</span>
              <span class="na">title</span><span class="pi">:</span> <span class="s">Ahmad Shadeed</span>

        <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">twitch-channels</span>
          <span class="na">channels</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="s">theprimeagen</span>
            <span class="pi">-</span> <span class="s">j_blow</span>
            <span class="pi">-</span> <span class="s">giantwaffle</span>
            <span class="pi">-</span> <span class="s">cohhcarnage</span>
            <span class="pi">-</span> <span class="s">christitustech</span>
            <span class="pi">-</span> <span class="s">EJ_SA</span>

    <span class="pi">-</span> <span class="na">size</span><span class="pi">:</span> <span class="s">full</span>
      <span class="na">widgets</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">group</span>
          <span class="na">widgets</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">hacker-news</span>
            <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">lobsters</span>

        <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">videos</span>
          <span class="na">channels</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="s">UCXuqSBlHAE6Xw-yeJA0Tunw</span> <span class="c1"># Linus Tech Tips</span>
            <span class="pi">-</span> <span class="s">UCR-DXc1voovS8nhAvccRZhg</span> <span class="c1"># Jeff Geerling</span>
            <span class="pi">-</span> <span class="s">UCsBjURrPoezykLs9EqgamOA</span> <span class="c1"># Fireship</span>
            <span class="pi">-</span> <span class="s">UCBJycsmduvYEL83R_U4JriQ</span> <span class="c1"># Marques Brownlee</span>
            <span class="pi">-</span> <span class="s">UCi8C7TNs2ohrc6hnRQ5Sn2w</span> <span class="c1"># Kai Lentit</span>

        <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">group</span>
          <span class="na">widgets</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">reddit</span>
              <span class="na">subreddit</span><span class="pi">:</span> <span class="s">technology</span>
              <span class="na">show-thumbnails</span><span class="pi">:</span> <span class="no">true</span>
            <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">reddit</span>
              <span class="na">subreddit</span><span class="pi">:</span> <span class="s">selfhosted</span>
              <span class="na">show-thumbnails</span><span class="pi">:</span> <span class="no">true</span>

    <span class="pi">-</span> <span class="na">size</span><span class="pi">:</span> <span class="s">small</span>
      <span class="na">widgets</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">weather</span>
          <span class="na">location</span><span class="pi">:</span> <span class="s">Woodbury, Minnesota, USA</span>
          <span class="na">units</span><span class="pi">:</span> <span class="s">imperial</span> <span class="c1"># alternatively "metric"</span>
          <span class="na">hour-format</span><span class="pi">:</span> <span class="s">12h</span> <span class="c1"># alternatively "24h"</span>
          <span class="c1"># Optionally hide the location from being displayed in the widget</span>
          <span class="c1"># hide-location: true</span>

        <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">markets</span>
          <span class="na">markets</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">symbol</span><span class="pi">:</span> <span class="s">SPY</span>
              <span class="na">name</span><span class="pi">:</span> <span class="s">S&amp;P </span><span class="m">500</span>
            <span class="pi">-</span> <span class="na">symbol</span><span class="pi">:</span> <span class="s">BSX</span>
              <span class="na">name</span><span class="pi">:</span> <span class="s">Boston Scientific</span>
            <span class="pi">-</span> <span class="na">symbol</span><span class="pi">:</span> <span class="s">MDT</span>
              <span class="na">name</span><span class="pi">:</span> <span class="s">Medtronic</span>
            <span class="pi">-</span> <span class="na">symbol</span><span class="pi">:</span> <span class="s">ABT</span>
              <span class="na">name</span><span class="pi">:</span> <span class="s">Abbott</span>
            <span class="pi">-</span> <span class="na">symbol</span><span class="pi">:</span> <span class="s">MSFT</span>
              <span class="na">name</span><span class="pi">:</span> <span class="s">Microsoft</span>

        <span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">releases</span>
          <span class="na">cache</span><span class="pi">:</span> <span class="s">1d</span>
          <span class="c1"># Without authentication the Github API allows for up to 60 requests per hour. You can create a</span>
          <span class="c1"># read-only token from your Github account settings and use it here to increase the limit.</span>
          <span class="c1"># token: ...</span>
          <span class="na">repositories</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="s">glanceapp/glance</span>
            <span class="pi">-</span> <span class="s">go-gitea/gitea</span>
            <span class="pi">-</span> <span class="s">immich-app/immich</span>
            <span class="pi">-</span> <span class="s">syncthing/syncthing</span>
</code></pre></div></div>

<p>Which I launched</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ docker compose up
Attaching to dynacat
dynacat  | 2026/03/28 16:45:45 Starting server on :8080 (base-url: "", assets-path: "/app/assets")
</code></pre></div></div>

<p>And we can see the weather and stocks look correct</p>

<p><a href="/content/images/2026/03/dynacat-02.png"><img src="/content/images/2026/03/dynacat-02.png" alt="/content/images/2026/03/dynacat-02.png" /></a></p>

<p>I’m finding it okay, but not sure what is the big difference with <a href="https://github.com/glanceapp/glance">glance</a> which I’ve had running at <a href="https://glance.tpk.pw/">https://glance.tpk.pw/</a> for quite some time, at least since I <a href="https://freshbrewed.science/2025/05/08/misctools.html">wrote about it</a> back in May 2025.</p>

<p>There are plenty of <a href="https://github.com/Panonim/dynacat/blob/main/docs/configuration.md#configuring-dynacat">available widgets</a> for Dynacat.</p>

<p>One that caught my eye was <a href="https://github.com/Panonim/dynacat/blob/main/docs/configuration.md#monitor">monitor</a></p>

<p>I added a block for some of my hosted services.  I found icons for all but Gancio</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>        - type: monitor
          cache: 1m
          title: Services
          sites:
            - title: Forgejo
              url: https://forgejo.freshbrewed.science
              icon: sh:forgejo
            - title: Gitea
              url: https://gitea.freshbrewed.science
              icon: sh:gitea
            - title: Immich
              url: https://photos.freshbrewed.science
              icon: sh:immich
            - title: Harbor
              url: https://harbor.freshbrewed.science
              icon: sh:harbor
            - title: Gancio
              url: https://gancio.tpk.pw
              icon: mdi:calendar-month
</code></pre></div></div>

<p>I would say that looks pretty clean</p>

<p><a href="/content/images/2026/03/dynacat-03.png"><img src="/content/images/2026/03/dynacat-03.png" alt="/content/images/2026/03/dynacat-03.png" /></a></p>

<p>Now, if I wanted to host this in k8s, I could convert the docker compose to a kubernetes manifest with PVCs.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">PersistentVolumeClaim</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">dynacat-config-pvc</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">accessModes</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">ReadWriteOnce</span>
  <span class="na">resources</span><span class="pi">:</span>
    <span class="na">requests</span><span class="pi">:</span>
      <span class="na">storage</span><span class="pi">:</span> <span class="s">1Gi</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">PersistentVolumeClaim</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">dynacat-assets-pvc</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">accessModes</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">ReadWriteOnce</span>
  <span class="na">resources</span><span class="pi">:</span>
    <span class="na">requests</span><span class="pi">:</span>
      <span class="na">storage</span><span class="pi">:</span> <span class="s">5Gi</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Secret</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">dynacat-secret</span>
<span class="na">type</span><span class="pi">:</span> <span class="s">Opaque</span>
<span class="na">stringData</span><span class="pi">:</span>
  <span class="na">MY_SECRET_TOKEN</span><span class="pi">:</span> <span class="s2">"</span><span class="s">12345"</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">dynacat</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">dynacat</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">1</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">dynacat</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app</span><span class="pi">:</span> <span class="s">dynacat</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">dynacat</span>
        <span class="na">image</span><span class="pi">:</span> <span class="s">panonim/dynacat</span>
        <span class="na">imagePullPolicy</span><span class="pi">:</span> <span class="s">IfNotPresent</span>
        <span class="na">ports</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">http</span>
          <span class="na">containerPort</span><span class="pi">:</span> <span class="m">8080</span>
          <span class="na">protocol</span><span class="pi">:</span> <span class="s">TCP</span>
        <span class="na">env</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">MY_SECRET_TOKEN</span>
          <span class="na">valueFrom</span><span class="pi">:</span>
            <span class="na">secretKeyRef</span><span class="pi">:</span>
              <span class="na">name</span><span class="pi">:</span> <span class="s">dynacat-secret</span>
              <span class="na">key</span><span class="pi">:</span> <span class="s">MY_SECRET_TOKEN</span>
        <span class="na">volumeMounts</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">config</span>
          <span class="na">mountPath</span><span class="pi">:</span> <span class="s">/app/config</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">assets</span>
          <span class="na">mountPath</span><span class="pi">:</span> <span class="s">/app/assets</span>
        <span class="na">resources</span><span class="pi">:</span>
          <span class="na">requests</span><span class="pi">:</span>
            <span class="na">memory</span><span class="pi">:</span> <span class="s2">"</span><span class="s">128Mi"</span>
            <span class="na">cpu</span><span class="pi">:</span> <span class="s2">"</span><span class="s">100m"</span>
          <span class="na">limits</span><span class="pi">:</span>
            <span class="na">memory</span><span class="pi">:</span> <span class="s2">"</span><span class="s">256Mi"</span>
            <span class="na">cpu</span><span class="pi">:</span> <span class="s2">"</span><span class="s">500m"</span>
      <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">config</span>
        <span class="na">persistentVolumeClaim</span><span class="pi">:</span>
          <span class="na">claimName</span><span class="pi">:</span> <span class="s">dynacat-config-pvc</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">assets</span>
        <span class="na">persistentVolumeClaim</span><span class="pi">:</span>
          <span class="na">claimName</span><span class="pi">:</span> <span class="s">dynacat-assets-pvc</span>
      <span class="na">restartPolicy</span><span class="pi">:</span> <span class="s">Always</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">dynacat-service</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">dynacat</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">ClusterIP</span>
  <span class="na">ports</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">http</span>
    <span class="na">port</span><span class="pi">:</span> <span class="m">80</span>
    <span class="na">targetPort</span><span class="pi">:</span> <span class="m">8080</span>
    <span class="na">protocol</span><span class="pi">:</span> <span class="s">TCP</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">dynacat</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">networking.k8s.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Ingress</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">dynacat-ingress</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">dynacat</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">ingressClassName</span><span class="pi">:</span> <span class="s">nginx</span>
  <span class="na">rules</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">host</span><span class="pi">:</span> <span class="s">dynacat.example.com</span>
    <span class="na">http</span><span class="pi">:</span>
      <span class="na">paths</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s">/</span>
        <span class="na">pathType</span><span class="pi">:</span> <span class="s">Prefix</span>
        <span class="na">backend</span><span class="pi">:</span>
          <span class="na">service</span><span class="pi">:</span>
            <span class="na">name</span><span class="pi">:</span> <span class="s">dynacat-service</span>
            <span class="na">port</span><span class="pi">:</span>
              <span class="na">number</span><span class="pi">:</span> <span class="m">80</span>
</code></pre></div></div>

<p>I’ll give it a try by setting an A record</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ az account set --subscription "Pay-As-You-Go" &amp;&amp; az network dns record-set a add-record -g idjdnsrg -z tpk.pw -a 76.156.69.232 -n dynacat
{
  "ARecords": [
    {
      "ipv4Address": "76.156.69.232"
    }
  ],
  "TTL": 3600,
  "etag": "c4823121-b5ed-48f0-9a8d-7fae02e910c3",
  "fqdn": "dynacat.tpk.pw.",
  "id": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/idjdnsrg/providers/Microsoft.Network/dnszones/tpk.pw/A/dynacat",
  "name": "dynacat",
  "provisioningState": "Succeeded",
  "resourceGroup": "idjdnsrg",
  "targetResource": {},
  "trafficManagementProfile": {},
  "type": "Microsoft.Network/dnszones/A"
</code></pre></div></div>

<p>I’ll tweak up the Ingress to use TLS and my A name</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: dynacat-ingress
  annotations:
    cert-manager.io/cluster-issuer: azuredns-tpkpw
    ingress.kubernetes.io/ssl-redirect: "true"
    kubernetes.io/tls-acme: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: "0"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.org/client-max-body-size: "0"
    nginx.org/proxy-connect-timeout: "3600"
    nginx.org/proxy-read-timeout: "3600"
    nginx.org/websocket-services: dynacat-service
  labels:
    app: dynacat
spec:
  ingressClassName: nginx
  rules:
  - host: dynacat.tpk.pw
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: dynacat-service
            port:
              number: 80
  tls:
  - hosts:
    - dynacat.tpk.pw
    secretName: dynacat-tls
</code></pre></div></div>

<p>Then apply</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl apply -f ./kubernetes-manifest.yaml
persistentvolumeclaim/dynacat-config-pvc created
persistentvolumeclaim/dynacat-assets-pvc created
secret/dynacat-secret created
deployment.apps/dynacat created
service/dynacat-service created
ingress.networking.k8s.io/dynacat-ingress created
</code></pre></div></div>

<p>Ahh - a bit of an issue - the pod needs the config to boot up, but I cant just kubectl cp to a crashing pod</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/dynacat/dynacat$ kubectl get po | grep dyna
dynacat-7f94f84bb9-gnh77                             0/1     CrashLoopBackOff   1 (10s ago)        19s
builder@DESKTOP-QADGF36:~/Workspaces/dynacat/dynacat$ kubectl logs dynacat-7f94f84bb9-gnh77
parsing config: reading /app/config/dynacat.yml: open /app/config/dynacat.yml: no such file or directory
</code></pre></div></div>

<p>I wasn’t sure the right approach - a utility pod to load, an init file, new Configmaps to mount so I sought some input from Claude and it suggested using some init pods to load at the start:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">ConfigMap</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">dynacat-config-files</span>
<span class="na">data</span><span class="pi">:</span>
  <span class="na">dynacat.yml</span><span class="pi">:</span> <span class="pi">|</span>
    <span class="s">server:</span>
      <span class="s">assets-path: /app/assets</span>

    <span class="s">theme:</span>
      <span class="s"># Note: assets are cached by the browser, changes to the CSS file</span>
      <span class="s"># will not be reflected until the browser cache is cleared (Ctrl+F5)</span>
      <span class="s">custom-css-file: /assets/user.css</span>

    <span class="s">pages:</span>
      <span class="s"># It's not necessary to create a new file for each page and include it, you can simply</span>
      <span class="s"># put its contents here, though multiple pages are easier to manage when separated</span>
      <span class="s">- $include: home.yml</span>
  <span class="na">home.yml</span><span class="pi">:</span> <span class="pi">|</span>
    <span class="s">- name: Home</span>
      <span class="s"># Optionally, if you only have a single page you can hide the desktop navigation for a cleaner look</span>
      <span class="s"># hide-desktop-navigation: true</span>
      <span class="s">columns:</span>
        <span class="s">- size: small</span>
          <span class="s">widgets:</span>
            <span class="s">- type: calendar</span>
              <span class="s">first-day-of-week: sunday</span>

            <span class="s">- type: rss</span>
              <span class="s">limit: 10</span>
              <span class="s">collapse-after: 3</span>
              <span class="s">cache: 12h</span>
              <span class="s">feeds:</span>
                <span class="s">- url: https://selfh.st/rss/</span>
                  <span class="s">title: selfh.st</span>
                <span class="s">- url: https://ciechanow.ski/atom.xml</span>
                <span class="s">- url: https://www.joshwcomeau.com/rss.xml</span>
                  <span class="s">title: Josh Comeau</span>
                <span class="s">- url: https://freshbrewed.science/feed.xml</span>
                <span class="s">- url: https://ishadeed.com/feed.xml</span>
                  <span class="s">title: Ahmad Shadeed</span>

            <span class="s">- type: twitch-channels</span>
              <span class="s">channels:</span>
                <span class="s">- theprimeagen</span>
                <span class="s">- j_blow</span>
                <span class="s">- giantwaffle</span>
                <span class="s">- cohhcarnage</span>
                <span class="s">- christitustech</span>
                <span class="s">- EJ_SA</span>

        <span class="s">- size: full</span>
          <span class="s">widgets:</span>
            <span class="s">- type: group</span>
              <span class="s">widgets:</span>
                <span class="s">- type: hacker-news</span>
                <span class="s">- type: lobsters</span>

            <span class="s">- type: videos</span>
              <span class="s">channels:</span>
                <span class="s">- UCXuqSBlHAE6Xw-yeJA0Tunw # Linus Tech Tips</span>
                <span class="s">- UCR-DXc1voovS8nhAvccRZhg # Jeff Geerling</span>
                <span class="s">- UCsBjURrPoezykLs9EqgamOA # Fireship</span>
                <span class="s">- UCBJycsmduvYEL83R_U4JriQ # Marques Brownlee</span>
                <span class="s">- UCi8C7TNs2ohrc6hnRQ5Sn2w # Kai Lentit</span>

            <span class="s">- type: group</span>
              <span class="s">widgets:</span>
                <span class="s">- type: reddit</span>
                  <span class="s">subreddit: technology</span>
                  <span class="s">show-thumbnails: true</span>
                <span class="s">- type: reddit</span>
                  <span class="s">subreddit: selfhosted</span>
                  <span class="s">show-thumbnails: true</span>
    
        <span class="s">- size: small</span>
          <span class="s">widgets:</span>
            <span class="s">- type: weather</span>
              <span class="s">location: Woodbury, Minnesota, USA</span>
              <span class="s">units: imperial # alternatively "metric"</span>
              <span class="s">hour-format: 12h # alternatively "24h"</span>
              <span class="s"># Optionally hide the location from being displayed in the widget</span>
              <span class="s"># hide-location: true</span>
    
            <span class="s">- type: monitor</span>
              <span class="s">cache: 1m</span>
              <span class="s">title: Services</span>
              <span class="s">sites:</span>
                <span class="s">- title: Forgejo</span>
                  <span class="s">url: https://forgejo.freshbrewed.science</span>
                  <span class="s">icon: sh:forgejo</span>
                <span class="s">- title: Gitea</span>
                  <span class="s">url: https://gitea.freshbrewed.science</span>
                  <span class="s">icon: sh:gitea</span>
                <span class="s">- title: Immich</span>
                  <span class="s">url: https://photos.freshbrewed.science</span>
                  <span class="s">icon: sh:immich</span>
                <span class="s">- title: Harbor</span>
                  <span class="s">url: https://harbor.freshbrewed.science</span>
                  <span class="s">icon: sh:harbor</span>
                <span class="s">- title: Gancio</span>
                  <span class="s">url: https://gancio.tpk.pw</span>
                  <span class="s">icon: mdi:calendar-month</span>
    
            <span class="s">- type: markets</span>
              <span class="s">markets:</span>
                <span class="s">- symbol: SPY</span>
                  <span class="s">name: S&amp;P 500</span>
                <span class="s">- symbol: BSX</span>
                  <span class="s">name: Boston Scientific</span>
                <span class="s">- symbol: MDT</span>
                  <span class="s">name: Medtronic</span>
                <span class="s">- symbol: ABT</span>
                  <span class="s">name: Abbott</span>
                <span class="s">- symbol: MSFT</span>
                  <span class="s">name: Microsoft</span>
    
            <span class="s">- type: releases</span>
              <span class="s">cache: 1d</span>
              <span class="s"># Without authentication the Github API allows for up to 60 requests per hour. You can create a</span>
              <span class="s"># read-only token from your Github account settings and use it here to increase the limit.</span>
              <span class="s"># token: ...</span>
              <span class="s">repositories:</span>
                <span class="s">- glanceapp/glance</span>
                <span class="s">- go-gitea/gitea</span>
                <span class="s">- immich-app/immich</span>
                <span class="s">- syncthing/syncthing</span>
<span class="s">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">PersistentVolumeClaim</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">dynacat-config-pvc</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">accessModes</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">ReadWriteOnce</span>
  <span class="na">resources</span><span class="pi">:</span>
    <span class="na">requests</span><span class="pi">:</span>
      <span class="na">storage</span><span class="pi">:</span> <span class="s">1Gi</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">PersistentVolumeClaim</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">dynacat-assets-pvc</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">accessModes</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">ReadWriteOnce</span>
  <span class="na">resources</span><span class="pi">:</span>
    <span class="na">requests</span><span class="pi">:</span>
      <span class="na">storage</span><span class="pi">:</span> <span class="s">5Gi</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Secret</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">dynacat-secret</span>
<span class="na">type</span><span class="pi">:</span> <span class="s">Opaque</span>
<span class="na">stringData</span><span class="pi">:</span>
  <span class="na">MY_SECRET_TOKEN</span><span class="pi">:</span> <span class="s2">"</span><span class="s">12345"</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">dynacat</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">dynacat</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">1</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">dynacat</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app</span><span class="pi">:</span> <span class="s">dynacat</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">initContainers</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">init-config</span>
        <span class="na">image</span><span class="pi">:</span> <span class="s">busybox:1.35</span>
        <span class="na">command</span><span class="pi">:</span> <span class="pi">[</span><span class="s1">'</span><span class="s">sh'</span><span class="pi">,</span> <span class="s1">'</span><span class="s">-c'</span><span class="pi">]</span>
        <span class="na">args</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="pi">|</span>
            <span class="s">cp /config-src/dynacat.yml /config-dst/dynacat.yml</span>
            <span class="s">cp /config-src/home.yml /config-dst/home.yml</span>
        <span class="na">volumeMounts</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">config-src</span>
          <span class="na">mountPath</span><span class="pi">:</span> <span class="s">/config-src</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">config</span>
          <span class="na">mountPath</span><span class="pi">:</span> <span class="s">/config-dst</span>
      <span class="na">containers</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">dynacat</span>
        <span class="na">image</span><span class="pi">:</span> <span class="s">panonim/dynacat</span>
        <span class="na">imagePullPolicy</span><span class="pi">:</span> <span class="s">IfNotPresent</span>
        <span class="na">ports</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">http</span>
          <span class="na">containerPort</span><span class="pi">:</span> <span class="m">8080</span>
          <span class="na">protocol</span><span class="pi">:</span> <span class="s">TCP</span>
        <span class="na">env</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">MY_SECRET_TOKEN</span>
          <span class="na">valueFrom</span><span class="pi">:</span>
            <span class="na">secretKeyRef</span><span class="pi">:</span>
              <span class="na">name</span><span class="pi">:</span> <span class="s">dynacat-secret</span>
              <span class="na">key</span><span class="pi">:</span> <span class="s">MY_SECRET_TOKEN</span>
        <span class="na">volumeMounts</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">config</span>
          <span class="na">mountPath</span><span class="pi">:</span> <span class="s">/app/config</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">assets</span>
          <span class="na">mountPath</span><span class="pi">:</span> <span class="s">/app/assets</span>
        <span class="na">resources</span><span class="pi">:</span>
          <span class="na">requests</span><span class="pi">:</span>
            <span class="na">memory</span><span class="pi">:</span> <span class="s2">"</span><span class="s">128Mi"</span>
            <span class="na">cpu</span><span class="pi">:</span> <span class="s2">"</span><span class="s">100m"</span>
          <span class="na">limits</span><span class="pi">:</span>
            <span class="na">memory</span><span class="pi">:</span> <span class="s2">"</span><span class="s">256Mi"</span>
            <span class="na">cpu</span><span class="pi">:</span> <span class="s2">"</span><span class="s">500m"</span>
      <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">config-src</span>
        <span class="na">configMap</span><span class="pi">:</span>
          <span class="na">name</span><span class="pi">:</span> <span class="s">dynacat-config-files</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">config</span>
        <span class="na">persistentVolumeClaim</span><span class="pi">:</span>
          <span class="na">claimName</span><span class="pi">:</span> <span class="s">dynacat-config-pvc</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">assets</span>
        <span class="na">persistentVolumeClaim</span><span class="pi">:</span>
          <span class="na">claimName</span><span class="pi">:</span> <span class="s">dynacat-assets-pvc</span>
      <span class="na">restartPolicy</span><span class="pi">:</span> <span class="s">Always</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">dynacat-service</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">dynacat</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">ClusterIP</span>
  <span class="na">ports</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">http</span>
    <span class="na">port</span><span class="pi">:</span> <span class="m">80</span>
    <span class="na">targetPort</span><span class="pi">:</span> <span class="m">8080</span>
    <span class="na">protocol</span><span class="pi">:</span> <span class="s">TCP</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">dynacat</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">networking.k8s.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Ingress</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">dynacat-ingress</span>
  <span class="na">annotations</span><span class="pi">:</span>
    <span class="na">cert-manager.io/cluster-issuer</span><span class="pi">:</span> <span class="s">azuredns-tpkpw</span>
    <span class="na">ingress.kubernetes.io/ssl-redirect</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
    <span class="na">kubernetes.io/tls-acme</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
    <span class="na">nginx.ingress.kubernetes.io/proxy-body-size</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0"</span>
    <span class="na">nginx.ingress.kubernetes.io/proxy-read-timeout</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3600"</span>
    <span class="na">nginx.ingress.kubernetes.io/proxy-send-timeout</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3600"</span>
    <span class="na">nginx.ingress.kubernetes.io/ssl-redirect</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
    <span class="na">nginx.org/client-max-body-size</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0"</span>
    <span class="na">nginx.org/proxy-connect-timeout</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3600"</span>
    <span class="na">nginx.org/proxy-read-timeout</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3600"</span>
    <span class="na">nginx.org/websocket-services</span><span class="pi">:</span> <span class="s">dynacat-service</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">dynacat</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">ingressClassName</span><span class="pi">:</span> <span class="s">nginx</span>
  <span class="na">rules</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">host</span><span class="pi">:</span> <span class="s">dynacat.tpk.pw</span>
    <span class="na">http</span><span class="pi">:</span>
      <span class="na">paths</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s">/</span>
        <span class="na">pathType</span><span class="pi">:</span> <span class="s">Prefix</span>
        <span class="na">backend</span><span class="pi">:</span>
          <span class="na">service</span><span class="pi">:</span>
            <span class="na">name</span><span class="pi">:</span> <span class="s">dynacat-service</span>
            <span class="na">port</span><span class="pi">:</span>
              <span class="na">number</span><span class="pi">:</span> <span class="m">80</span>
  <span class="na">tls</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">hosts</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">dynacat.tpk.pw</span>
    <span class="na">secretName</span><span class="pi">:</span> <span class="s">dynacat-tls</span>
</code></pre></div></div>

<p>I found Nginx gets a bit stuck sometimes on ingress updates, so I’ll delete the Ingress then apply</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl delete ingress dynacat-ingress
ingress.networking.k8s.io "dynacat-ingress" deleted
$ kubectl apply -f ./kubernetes-manifest.yaml
configmap/dynacat-config-files created
persistentvolumeclaim/dynacat-config-pvc unchanged
persistentvolumeclaim/dynacat-assets-pvc unchanged
secret/dynacat-secret configured
deployment.apps/dynacat configured
service/dynacat-service unchanged
ingress.networking.k8s.io/dynacat-ingress created
</code></pre></div></div>

<p>This time it worked</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dynacat-846f8b69c7-g7mnq                             1/1     Running            0                   26s
</code></pre></div></div>

<p>The cert was already good</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get cert | grep dyna
dynacat-tls                         True    dynacat-tls                         74s
</code></pre></div></div>

<p>This time the website loaded</p>

<p><a href="/content/images/2026/03/dynacat-04.png"><img src="/content/images/2026/03/dynacat-04.png" alt="/content/images/2026/03/dynacat-04.png" /></a></p>

<h1 id="seafile">Seafile</h1>

<p>Another interesting app I bookmarked from <a href="https://mariushosting.com/how-to-install-seafile-13-on-your-synology-nas/">a Marius post</a> is <a href="https://manual.seafile.com/latest/">Seafile</a>.</p>

<p>What surprised me right way was that the Seafile <a href="https://manual.seafile.com/latest/setup/setup_ce_by_docker/">documentation</a> has many gaps.</p>

<p>It talks in detail about the docker compose envs, but lacks showing a docker compose file.  <a href="https://manual.seafile.com/latest/">This page</a> based on links used to show a compose YAML.</p>

<p>In this case we are going to use the Docker Compose from <a href="https://mariushosting.com/how-to-install-seafile-13-on-your-synology-nas/">the MariusHosting article</a></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">db</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">mariadb:11.8-noble</span> <span class="c1">#LTS Long Time Support Until October 15, 2033.</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">Seafile-DB</span>
    <span class="na">hostname</span><span class="pi">:</span> <span class="s">seafile-db</span>
    <span class="na">security_opt</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">no-new-privileges:false</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/volume1/docker/seafile/db:/var/lib/mysql:rw</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">MYSQL_ROOT_PASSWORD</span><span class="pi">:</span> <span class="s">rootpass</span>
      <span class="na">MYSQL_DATABASE</span><span class="pi">:</span> <span class="s">seafile_db</span>
      <span class="na">MYSQL_USER</span><span class="pi">:</span> <span class="s">seafileuser</span>
      <span class="na">MYSQL_PASSWORD</span><span class="pi">:</span> <span class="s">seafilepassword</span>
      <span class="na">TZ</span><span class="pi">:</span> <span class="s">America/Chicago</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">on-failure:5</span>

  <span class="na">cache</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">memcached:1.6</span>
    <span class="na">entrypoint</span><span class="pi">:</span> <span class="s">memcached -m </span><span class="m">256</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">Seafile-CACHE</span>
    <span class="na">hostname</span><span class="pi">:</span> <span class="s">memcached</span>
    <span class="na">security_opt</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">no-new-privileges:true</span>
    <span class="na">read_only</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">user</span><span class="pi">:</span> <span class="s">1026:100</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">on-failure:5</span>
    
  <span class="na">redis</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">redis</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">Seafile-REDIS</span>
    <span class="na">command</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/bin/sh</span>
      <span class="pi">-</span> <span class="s">-c</span>
      <span class="pi">-</span> <span class="s">redis-server --requirepass redispass</span>
    <span class="na">hostname</span><span class="pi">:</span> <span class="s">redis</span>
    <span class="na">security_opt</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">no-new-privileges:true</span>
    <span class="na">read_only</span><span class="pi">:</span> <span class="no">false</span>
    <span class="na">user</span><span class="pi">:</span> <span class="s">1026:100</span>
    <span class="na">healthcheck</span><span class="pi">:</span>
      <span class="na">test</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">CMD-SHELL"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">redis-cli</span><span class="nv"> </span><span class="s">ping</span><span class="nv"> </span><span class="s">||</span><span class="nv"> </span><span class="s">exit</span><span class="nv"> </span><span class="s">1"</span><span class="pi">]</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/volume1/docker/seafile/redis:/data:rw</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">TZ</span><span class="pi">:</span> <span class="s">America/Chicago</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">on-failure:5</span>
    
  <span class="na">seafile</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">seafileltd/seafile-mc:13.0-latest</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">Seafile</span>
    <span class="na">user</span><span class="pi">:</span> <span class="s">0:0</span>
    <span class="na">hostname</span><span class="pi">:</span> <span class="s">seafile</span>
    <span class="na">security_opt</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">no-new-privileges:false</span>
    <span class="na">healthcheck</span><span class="pi">:</span>
      <span class="na">test</span><span class="pi">:</span> <span class="s">wget --no-verbose --tries=1 --spider http://localhost</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/volume1/docker/seafile/data:/shared:rw</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">8611:80</span>
    <span class="na">environment</span><span class="pi">:</span>
       <span class="na">INIT_SEAFILE_MYSQL_ROOT_PASSWORD</span><span class="pi">:</span> <span class="s">rootpass</span>
       <span class="na">SEAFILE_MYSQL_DB_HOST</span><span class="pi">:</span> <span class="s">seafile-db</span>
       <span class="na">SEAFILE_MYSQL_DB_USER</span><span class="pi">:</span> <span class="s">seafileuser</span>
       <span class="na">SEAFILE_MYSQL_DB_PORT</span><span class="pi">:</span> <span class="m">3306</span>
       <span class="na">SEAFILE_MYSQL_DB_PASSWORD</span><span class="pi">:</span> <span class="s">seafilepassword</span>
       <span class="na">SEAFILE_MYSQL_DB_SEAFILE_DB_NAME</span><span class="pi">:</span> <span class="s">seafile_db</span>
       <span class="na">SEAFILE_MYSQL_DB_CCNET_DB_NAME</span><span class="pi">:</span> <span class="s">ccnet_db</span>
       <span class="na">SEAFILE_MYSQL_DB_SEAHUB_DB_NAME</span><span class="pi">:</span> <span class="s">seahub_db</span>
       <span class="na">CACHE_PROVIDER</span><span class="pi">:</span> <span class="s">redis</span>
       <span class="na">REDIS_HOST</span><span class="pi">:</span> <span class="s">redis</span>
       <span class="na">REDIS_PORT</span><span class="pi">:</span> <span class="m">6379</span>
       <span class="na">REDIS_PASSWORD</span><span class="pi">:</span> <span class="s">redispass</span>
       <span class="na">TIME_ZONE</span><span class="pi">:</span> <span class="s">America/Chicago</span>
       <span class="na">SEAFILE_VOLUME</span><span class="pi">:</span> <span class="s">/opt/seafile-data</span>
       <span class="na">SEAFILE_MYSQL_VOLUME</span><span class="pi">:</span> <span class="s">/opt/seafile-mysql/db</span>
       <span class="na">INIT_SEAFILE_ADMIN_EMAIL</span><span class="pi">:</span> <span class="s">isaac.johnson@gmail.com</span>
       <span class="na">INIT_SEAFILE_ADMIN_PASSWORD</span><span class="pi">:</span> <span class="s">notmypassword</span>
       <span class="na">JWT_PRIVATE_KEY</span><span class="pi">:</span> <span class="s">dOxZYTTZgXKMHkqLBIQVImayQXAVWdzGBPuFJKggzcgvgPJPXpWzqzKaUOIOGGIr</span>
       <span class="na">SEADOC_VOLUME</span><span class="pi">:</span> <span class="s">/opt/seadoc-data</span>
       <span class="na">SEADOC_IMAGE</span><span class="pi">:</span> <span class="s">seafileltd/sdoc-server:2.0-latest</span>
       <span class="na">ENABLE_SEADOC</span><span class="pi">:</span> <span class="no">false</span> <span class="c1">#or true</span>
       <span class="na">SEADOC_SERVER_URL</span><span class="pi">:</span> <span class="s">https://seafile.tpk.pw/sdoc-server</span>
       <span class="na">SEAFILE_SERVER_HOSTNAME</span><span class="pi">:</span> <span class="s">seafile.tpk.pw</span>
       <span class="na">SEAFILE_SERVER_PROTOCOL</span><span class="pi">:</span> <span class="s">https</span>
       <span class="na">FORCE_HTTPS_IN_CONF</span><span class="pi">:</span> <span class="no">true</span>
       <span class="na">SEAFILE_SERVER_LETSENCRYPT</span><span class="pi">:</span> <span class="no">false</span>
    <span class="na">depends_on</span><span class="pi">:</span>
      <span class="na">db</span><span class="pi">:</span>
        <span class="na">condition</span><span class="pi">:</span> <span class="s">service_started</span>
      <span class="na">cache</span><span class="pi">:</span>
        <span class="na">condition</span><span class="pi">:</span> <span class="s">service_started</span>
      <span class="na">redis</span><span class="pi">:</span>
        <span class="na">condition</span><span class="pi">:</span> <span class="s">service_started</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">on-failure:5</span>
</code></pre></div></div>

<p>I’ll want to forward traffic here, so right off the bat, I’ll create an A Record in Azure DNS</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ az account set --subscription "Pay-As-You-Go" &amp;&amp; az network dns record-set a add-record -g idjdnsrg -z tpk.pw -a 76.156.69.232 -n seafile
{
  "ARecords": [
    {
      "ipv4Address": "76.156.69.232"
    }
  ],
  "TTL": 3600,
  "etag": "2e615ee2-6bd7-4e42-8bec-e288fada805c",
  "fqdn": "seafile.tpk.pw.",
  "id": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/idjdnsrg/providers/Microsoft.Network/dnszones/tpk.pw/A/seafile",
  "name": "seafile",
  "provisioningState": "Succeeded",
  "resourceGroup": "idjdnsrg",
  "targetResource": {},
  "trafficManagementProfile": {},
  "type": "Microsoft.Network/dnszones/A"
}
</code></pre></div></div>

<p>I can now fire up Docker compose on the docker host (192.168.1.143 in this case)</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@bosgamerz9:~/Workspaces/Seafile$ docker compose up -d
[+] Running 38/38
 ✔ cache Pulled                                                                                                                                   10.9s
   ✔ ec781dee3f47 Pull complete                                                                                                                    9.7s
   ✔ 51dec6de03b1 Pull complete                                                                                                                    9.7s
   ✔ e6a929ea558d Pull complete                                                                                                                    9.7s
   ✔ b8f4f23b254e Pull complete                                                                                                                    9.8s
   ✔ 6ae38152dcb8 Pull complete                                                                                                                    9.8s
   ✔ e839fccf1879 Pull complete                                                                                                                    9.8s
 ✔ db Pulled                                                                                                                                       3.9s
   ✔ 817807f3c64e Pull complete                                                                                                                    1.7s
   ✔ 5843c951070e Pull complete                                                                                                                    1.7s
   ✔ 61a0e6ba478b Pull complete                                                                                                                    1.8s
   ✔ c688148c30c1 Pull complete                                                                                                                    1.8s
   ✔ 86447b1c7e20 Pull complete                                                                                                                    1.8s
   ✔ 943dc8eb543e Pull complete                                                                                                                    2.9s
   ✔ af136b8199a6 Pull complete                                                                                                                    2.9s
   ✔ 7761d3ebebab Pull complete                                                                                                                    2.9s
 ✔ seafile Pulled                                                                                                                                 14.7s
   ✔ 505b3596871d Pull complete                                                                                                                    5.8s
   ✔ c083fef1fc73 Pull complete                                                                                                                    5.9s
   ✔ be8b84cba34b Pull complete                                                                                                                    6.8s
   ✔ 8bedc8e9293a Pull complete                                                                                                                    6.9s
   ✔ 622aaad4b3db Pull complete                                                                                                                    7.1s
   ✔ b873e65fb3d4 Pull complete                                                                                                                    7.8s
   ✔ 1d2e2524dcdd Pull complete                                                                                                                    8.4s
   ✔ 601ae82f9133 Pull complete                                                                                                                    9.7s
   ✔ 35a091cd3e29 Pull complete                                                                                                                   11.5s
   ✔ fadc521a35fb Pull complete                                                                                                                   11.5s
   ✔ 55d2f36eefb8 Pull complete                                                                                                                   11.5s
   ✔ b9383a1b21d0 Pull complete                                                                                                                   11.5s
   ✔ c18edc61dfc9 Pull complete                                                                                                                   11.5s
   ✔ 8ee4a48da408 Pull complete                                                                                                                   13.6s
 ✔ redis Pulled                                                                                                                                   11.5s
   ✔ 5f7274725e4f Pull complete                                                                                                                    9.7s
   ✔ f4f2f7018ed9 Pull complete                                                                                                                    9.7s
   ✔ 3f63903b0cb8 Pull complete                                                                                                                   10.4s
   ✔ c9ff57cee690 Pull complete                                                                                                                   10.4s
   ✔ 4f4fb700ef54 Pull complete                                                                                                                   10.4s
   ✔ 3e6b2202a764 Pull complete                                                                                                                   10.5s
[+] Running 5/5
 ✔ Network seafile_default  Created                                                                                                                0.0s
 ✔ Container Seafile-REDIS  Started                                                                                                                0.6s
 ✔ Container Seafile-DB     Started                                                                                                                0.5s
 ✔ Container Seafile-CACHE  Started                                                                                                                0.6s
 ✔ Container Seafile        Started                                                                                                                0.3s
</code></pre></div></div>

<p>Then fire up a kubernetes ingress pointer manifest that should include an Endpoint, Service, and Ingress object for the A record</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~$ cat seafile.ingress.yaml
apiVersion: v1
kind: Endpoints
metadata:
  name: seafile-external-ip
subsets:
- addresses:
  - ip: 192.168.1.143
  ports:
  - name: seafileint
    port: 8611
    protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
  name: seafile-external-ip
spec:
  clusterIP: None
  clusterIPs:
  - None
  internalTrafficPolicy: Cluster
  ipFamilies:
  - IPv4
  - IPv6
  ipFamilyPolicy: RequireDualStack
  ports:
  - name: seafile
    port: 80
    protocol: TCP
    targetPort: 8611
  sessionAffinity: None
  type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: azuredns-tpkpw
    ingress.kubernetes.io/ssl-redirect: "true"
    kubernetes.io/tls-acme: "true"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.org/websocket-services: seafile-external-ip
  generation: 1
  name: seafileingress
spec:
  ingressClassName: nginx
  rules:
  - host: seafile.tpk.pw
    http:
      paths:
      - backend:
          service:
            name: seafile-external-ip
            port:
              number: 80
        path: /
        pathType: ImplementationSpecific
  tls:
  - hosts:
    - seafile.tpk.pw
    secretName: seafile-tls
builder@DESKTOP-QADGF36:~$ kubectl apply -f ./seafile.ingress.yaml
endpoints/seafile-external-ip created
service/seafile-external-ip created
ingress.networking.k8s.io/seafileingress created
</code></pre></div></div>

<p>When I see the cert is satisfied</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~$ kubectl get cert seafile-tls
NAME          READY   SECRET        AGE
seafile-tls   False   seafile-tls   27s
builder@DESKTOP-QADGF36:~$ kubectl get cert seafile-tls
NAME          READY   SECRET        AGE
seafile-tls   False   seafile-tls   59s
builder@DESKTOP-QADGF36:~$ kubectl get cert seafile-tls
NAME          READY   SECRET        AGE
seafile-tls   True    seafile-tls   83s
</code></pre></div></div>

<p>I can access the portal page</p>

<p><a href="/content/images/2026/03/seafile-01.png"><img src="/content/images/2026/03/seafile-01.png" alt="/content/images/2026/03/seafile-01.png" /></a></p>

<p>I logged in with the admin account to see the welcome splash</p>

<p><a href="/content/images/2026/03/seafile-02.png"><img src="/content/images/2026/03/seafile-02.png" alt="/content/images/2026/03/seafile-02.png" /></a></p>

<p>From here, we can see we already have a “My Library” created</p>

<p><a href="/content/images/2026/03/seafile-03.png"><img src="/content/images/2026/03/seafile-03.png" alt="/content/images/2026/03/seafile-03.png" /></a></p>

<p>It’s a little strange in that I can “create” a blank PPTX file</p>

<p><a href="/content/images/2026/03/seafile-04.png"><img src="/content/images/2026/03/seafile-04.png" alt="/content/images/2026/03/seafile-04.png" /></a></p>

<p>Then download it</p>

<p><a href="/content/images/2026/03/seafile-05.png"><img src="/content/images/2026/03/seafile-05.png" alt="/content/images/2026/03/seafile-05.png" /></a></p>

<p>which, indeed, made an empty PPTX</p>

<p><a href="/content/images/2026/03/seafile-06.png"><img src="/content/images/2026/03/seafile-06.png" alt="/content/images/2026/03/seafile-06.png" /></a></p>

<p>For a test, I filled in a couple slides</p>

<p><a href="/content/images/2026/03/seafile-07.png"><img src="/content/images/2026/03/seafile-07.png" alt="/content/images/2026/03/seafile-07.png" /></a></p>

<p>Uploading it back noted it would be replaced</p>

<p><a href="/content/images/2026/03/seafile-08.png"><img src="/content/images/2026/03/seafile-08.png" alt="/content/images/2026/03/seafile-08.png" /></a></p>

<p>That said, a quick upload showed the new file was there</p>

<p><a href="/content/images/2026/03/seafile-09.png"><img src="/content/images/2026/03/seafile-09.png" alt="/content/images/2026/03/seafile-09.png" /></a></p>

<p>That said, there is a history section and it looks like I can restore old copies</p>

<p><a href="/content/images/2026/03/seafile-10.png"><img src="/content/images/2026/03/seafile-10.png" alt="/content/images/2026/03/seafile-10.png" /></a></p>

<p>I tried loading a larger file (3.6Mb) and it stalled out</p>

<p><a href="/content/images/2026/03/seafile-11.png"><img src="/content/images/2026/03/seafile-11.png" alt="/content/images/2026/03/seafile-11.png" /></a></p>

<p>A refresh showed it crashed</p>

<p><a href="/content/images/2026/03/seafile-12.png"><img src="/content/images/2026/03/seafile-12.png" alt="/content/images/2026/03/seafile-12.png" /></a></p>

<p>It wasn’t coming back, yet the logs showed no errors</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CONTAINER ID   IMAGE                                          COMMAND                  CREATED          STATUS                        PORTS                                                                                                     NAMES
f3e3f2b0e9c9   seafileltd/seafile-mc:13.0-latest              "/sbin/my_init -- /s…"   22 minutes ago   Up 22 minutes (unhealthy)     0.0.0.0:8611-&gt;80/tcp, [::]:8611-&gt;80/tcp                                                                   Seafile
5eb404efd7f1   memcached:1.6                                  "memcached -m 256"       22 minutes ago   Up 22 minutes                 11211/tcp                                                                                                 Seafile-CACHE
e9182e277087   mariadb:11.8-noble                             "docker-entrypoint.s…"   22 minutes ago   Up 22 minutes                 3306/tcp                                                                                                  Seafile-DB
c60aa6ab51bc   redis                                          "docker-entrypoint.s…"   22 minutes ago   Up 22 minutes (healthy)       6379/tcp                                                                                                  Seafile-REDIS
</code></pre></div></div>

<p>with the main container logs</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@bosgamerz9:~/Workspaces/Seafile$ docker logs Seafile
*** Running /etc/my_init.d/01_create_data_links.sh...
*** Booting runit daemon...
*** Runit started as PID 11
*** Running /scripts/enterpoint.sh...
2026-03-30 05:56:28 Waiting Nginx
nginx: [warn] conflicting server name "" on 0.0.0.0:80, ignored
2026-03-30 05:56:28 Nginx ready
2026-03-30 05:56:28 This is an idle script (infinite loop) to keep container running.
nginx: [warn] conflicting server name "" on 0.0.0.0:80, ignored
[2026-03-30 05:56:37] Now running setup-seafile-mysql.py in auto mode.
Checking python on this machine ...


verifying password of user root ...  done

verifying password of user seafileuser ...  done

---------------------------------
This is your configuration
---------------------------------

    server name:            seafile
    server ip/domain:       seafile.tpk.pw

    seafile data dir:       /opt/seafile/seafile-data
    fileserver port:        8082

    database:               create new
    ccnet database:         ccnet_db
    seafile database:       seafile_db
    seahub database:        seahub_db
    database user:          seafileuser


Generating seafile configuration ...

done
Generating seahub configuration ...

----------------------------------------
Now creating seafevents database tables ...

----------------------------------------
----------------------------------------
Now creating ccnet database tables ...

----------------------------------------
----------------------------------------
Now creating seafile database tables ...

----------------------------------------
----------------------------------------
Now creating seahub database tables ...

----------------------------------------

creating seafile-server-latest symbolic link ...  done




-----------------------------------------------------------------
Your seafile server configuration has been finished successfully.
-----------------------------------------------------------------

run seafile server:     ./seafile.sh { start | stop | restart }
run seahub  server:     ./seahub.sh  { start &lt;port&gt; | stop | restart &lt;port&gt; }

-----------------------------------------------------------------
If you are behind a firewall, remember to allow input/output of these tcp ports:
-----------------------------------------------------------------

port of seafile fileserver:   8082
port of seahub:               8000

When problems occur, Refer to

        https://download.seafile.com/published/seafile-manual/home.md

for information.


[2026-03-30 05:56:39] Updating version stamp

Starting seafile server, please wait ...
Seafile server started

Done.

Starting seahub at port 8000 ...



----------------------------------------
Successfully created seafile admin
----------------------------------------




Seahub is started

Done.
</code></pre></div></div>

<p>I tried a restart</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@bosgamerz9:~/Workspaces/Seafile$ docker restart Seafile
Seafile

builder@bosgamerz9:~/Workspaces/Seafile$ docker logs Seafile | tail -n 10
*** Running /etc/my_init.d/01_create_data_links.sh...
*** Booting runit daemon...
*** Runit started as PID 11
*** Running /scripts/enterpoint.sh...
nginx: [warn] conflicting server name "" on 0.0.0.0:80, ignored
[2026-03-30 05:56:37] Now running setup-seafile-mysql.py in auto mode.
[2026-03-30 05:56:39] Updating version stamp
*** Shutting down /scripts/enterpoint.sh (PID 12)...
*** Shutting down runit daemon (PID 11)...
*** Running /etc/my_init.post_shutdown.d/10_syslog-ng.shutdown...
*** Init system aborted.
*** Killing all processes...
*** Running /etc/my_init.d/01_create_data_links.sh...
*** Booting runit daemon...
*** Runit started as PID 15
*** Running /scripts/enterpoint.sh...
nginx: [warn] conflicting server name "" on 0.0.0.0:80, ignored
[2026-03-30 06:19:53] Skip running setup-seafile-mysql.py because there is existing seafile-data folder.
Seafile server started

Done.

Starting seahub at port 8000 ...

Seahub is started

Done.

</code></pre></div></div>

<p>But still no go</p>

<p><a href="/content/images/2026/03/seafile-13.png"><img src="/content/images/2026/03/seafile-13.png" alt="/content/images/2026/03/seafile-13.png" /></a></p>

<p>My next try was a full stop and start</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@bosgamerz9:~/Workspaces/Seafile$ docker compose down
[+] Running 5/5
 ✔ Container Seafile        Removed                                                                                                                1.6s
 ✔ Container Seafile-REDIS  Removed                                                                                                               10.1s
 ✔ Container Seafile-CACHE  Removed                                                                                                                0.5s
 ✔ Container Seafile-DB     Removed                                                                                                                0.5s
 ✔ Network seafile_default  Removed                                                                                                                0.1s
builder@bosgamerz9:~/Workspaces/Seafile$ docker compose up
[+] Running 5/5
 ✔ Network seafile_default  Created                                                                                                                0.0s
 ✔ Container Seafile-DB     Created                                                                                                                0.0s
 ✔ Container Seafile-REDIS  Created                                                                                                                0.0s
 ✔ Container Seafile-CACHE  Created                                                                                                                0.0s
 ✔ Container Seafile        Created                                                                                                                0.0s
Attaching to Seafile, Seafile-CACHE, Seafile-DB, Seafile-REDIS
Seafile-REDIS  | 10:C 30 Mar 2026 06:21:49.430 # WARNING Memory overcommit must be enabled! Without it, a background save or replication may fail under low memory condition. Being disabled, it can also cause failures without low memory condition, see https://github.com/jemalloc/jemalloc/issues/1328. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
Seafile-REDIS  | 10:C 30 Mar 2026 06:21:49.430 * oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
Seafile-REDIS  | 10:C 30 Mar 2026 06:21:49.430 * Redis version=8.6.2, bits=64, commit=00000000, modified=1, pid=10, just started
Seafile-REDIS  | 10:C 30 Mar 2026 06:21:49.430 * Configuration loaded
Seafile-REDIS  | 10:M 30 Mar 2026 06:21:49.430 * monotonic clock: POSIX clock_gettime
Seafile-REDIS  | 10:M 30 Mar 2026 06:21:49.431 * Running mode=standalone, port=6379.
Seafile-REDIS  | 10:M 30 Mar 2026 06:21:49.431 * Server initialized
Seafile-REDIS  | 10:M 30 Mar 2026 06:21:49.431 * Ready to accept connections tcp
Seafile-DB     | 2026-03-30 06:21:49-05:00 [Note] [Entrypoint]: Entrypoint script for MariaDB Server 1:11.8.6+maria~ubu2404 started.
Seafile        | *** Running /etc/my_init.d/01_create_data_links.sh...
Seafile        | *** Booting runit daemon...
Seafile        | *** Runit started as PID 15
Seafile        | *** Running /scripts/enterpoint.sh...
Seafile        | 2026-03-30 06:21:49 Waiting Nginx
Seafile        | nginx: [warn] conflicting server name "" on 0.0.0.0:80, ignored
Seafile-DB     | 2026-03-30 06:21:49-05:00 [Warn] [Entrypoint]: /sys/fs/cgroup///memory.pressure not writable, functionality unavailable to MariaDB
Seafile-DB     | 2026-03-30 06:21:49-05:00 [Note] [Entrypoint]: Switching to dedicated user 'mysql'
Seafile-DB     | 2026-03-30 06:21:49-05:00 [Note] [Entrypoint]: Entrypoint script for MariaDB Server 1:11.8.6+maria~ubu2404 started.
Seafile        | 2026-03-30 06:21:49 Nginx ready
Seafile        | 2026-03-30 06:21:49 This is an idle script (infinite loop) to keep container running.
Seafile-DB     | 2026-03-30 06:21:49-05:00 [Note] [Entrypoint]: MariaDB upgrade not required
Seafile-DB     | 2026-03-30  6:21:49 0 [Note] Starting MariaDB 11.8.6-MariaDB-ubu2404 source revision 9bfea48ce1214cc4470f6f6f8a4e30352cef84e7 server_uid YKPNagnfIROmJPf794u8unHaCSA= as process 1
Seafile-DB     | 2026-03-30  6:21:49 0 [Note] InnoDB: Compressed tables use zlib 1.3
Seafile-DB     | 2026-03-30  6:21:49 0 [Note] InnoDB: Number of transaction pools: 1
Seafile-DB     | 2026-03-30  6:21:49 0 [Note] InnoDB: Using crc32 + pclmulqdq instructions
Seafile-DB     | 2026-03-30  6:21:49 0 [Warning] mariadbd: io_uring_queue_init() failed with EPERM: sysctl kernel.io_uring_disabled has the value 2, or 1 and the user of the process is not a member of sysctl kernel.io_uring_group. (see man 2 io_uring_setup).
Seafile-DB     | create_uring failed: falling back to libaio
Seafile-DB     | 2026-03-30  6:21:49 0 [Note] InnoDB: Using Linux native AIO
Seafile-DB     | 2026-03-30  6:21:49 0 [Note] InnoDB: innodb_buffer_pool_size_max=128m, innodb_buffer_pool_size=128m
Seafile-DB     | 2026-03-30  6:21:49 0 [Note] InnoDB: Completed initialization of buffer pool
Seafile-DB     | 2026-03-30  6:21:49 0 [Note] InnoDB: File system buffers for log disabled (block size=512 bytes)
Seafile        | nginx: [warn] conflicting server name "" on 0.0.0.0:80, ignored
Seafile-DB     | 2026-03-30  6:21:49 0 [Note] InnoDB: End of log at LSN=1297842
Seafile-DB     | 2026-03-30  6:21:49 0 [Note] InnoDB: Opened 3 undo tablespaces
Seafile-DB     | 2026-03-30  6:21:49 0 [Note] InnoDB: 128 rollback segments in 3 undo tablespaces are active.
Seafile-DB     | 2026-03-30  6:21:49 0 [Note] InnoDB: Setting file './ibtmp1' size to 12.000MiB. Physically writing the file full; Please wait ...
Seafile-DB     | 2026-03-30  6:21:49 0 [Note] InnoDB: File './ibtmp1' size is now 12.000MiB.
Seafile-DB     | 2026-03-30  6:21:49 0 [Note] InnoDB: log sequence number 1297842; transaction id 1045
Seafile-DB     | 2026-03-30  6:21:49 0 [Note] InnoDB: Loading buffer pool(s) from /var/lib/mysql/ib_buffer_pool
Seafile-DB     | 2026-03-30  6:21:49 0 [Note] Plugin 'FEEDBACK' is disabled.
Seafile-DB     | 2026-03-30  6:21:49 0 [Note] Plugin 'wsrep-provider' is disabled.
Seafile-DB     | 2026-03-30  6:21:49 0 [Note] InnoDB: Buffer pool(s) load completed at 260330  6:21:49
Seafile-DB     | 2026-03-30  6:21:51 0 [Note] Server socket created on IP: '0.0.0.0', port: '3306'.
Seafile-DB     | 2026-03-30  6:21:51 0 [Note] Server socket created on IP: '::', port: '3306'.
Seafile-DB     | 2026-03-30  6:21:51 0 [Note] mariadbd: Event Scheduler: Loaded 0 events
Seafile-DB     | 2026-03-30  6:21:51 0 [Note] mariadbd: ready for connections.
Seafile-DB     | Version: '11.8.6-MariaDB-ubu2404'  socket: '/run/mysqld/mysqld.sock'  port: 3306  mariadb.org binary distribution
Seafile        | [2026-03-30 06:21:51] Skip running setup-seafile-mysql.py because there is existing seafile-data folder.
Seafile        | waiting for mysql server to be ready: mysql is not ready
Seafile        | [03/30/2026 06:21:51][upgrade]: The container was recreated, start fix the media symlinks
Seafile        | mv: not replacing '/shared/seafile/seahub-data/avatars/default-non-register.jpg'
Seafile        | mv: not replacing '/shared/seafile/seahub-data/avatars/default.png'
Seafile        | mv: not replacing '/shared/seafile/seahub-data/avatars/groups'
Seafile        | [03/30/2026 06:21:51][upgrade]: Done
Seafile        |
Seafile        | Starting seafile server, please wait ...


Seafile        | Seafile server started
Seafile        |
Seafile        | Done.
Seafile        |
Seafile        | Starting seahub at port 8000 ...
Seafile        |
Seafile        | Seahub is started
Seafile        |
Seafile        | Done.
Seafile        |

</code></pre></div></div>

<p>That worked, and my last file was still there.  I tried uploading again</p>

<p><a href="/content/images/2026/03/seafile-14.png"><img src="/content/images/2026/03/seafile-14.png" alt="/content/images/2026/03/seafile-14.png" /></a></p>

<p>I tried Firefox and chrome, but without luck.  It was not uploading anymore</p>

<p><a href="/content/images/2026/03/seafile-15.png"><img src="/content/images/2026/03/seafile-15.png" alt="/content/images/2026/03/seafile-15.png" /></a></p>

<p>I also tried logging in directly to the app using the local URL, but it choked on uploads</p>

<p><a href="/content/images/2026/03/seafile-16.png"><img src="/content/images/2026/03/seafile-16.png" alt="/content/images/2026/03/seafile-16.png" /></a></p>

<p>Last shot, a full destroy then fire up</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># cleanup local files
builder@bosgamerz9:/volume1$ cd docker/
builder@bosgamerz9:/volume1/docker$ ls
seafile
builder@bosgamerz9:/volume1/docker$ sudo rm -rf ./seafile/
builder@bosgamerz9:/volume1/docker$ sudo mkdir seafile
builder@bosgamerz9:/volume1/docker$ sudo chmod 777 seafile/


# Now docker destroy
builder@bosgamerz9:~/Workspaces/Seafile$ docker compose down -v --rmi all --remove-orphans
[+] Running 4/4
 ✔ Image seafileltd/seafile-mc:13.0-latest  Removed                                                                                                1.2s
 ✔ Image mariadb:11.8-noble                 Removed                                                                                                1.4s
 ✔ Image memcached:1.6                      Removed                                                                                                0.1s
 ✔ Image redis:latest                       Removed                                                                                                0.0s
builder@bosgamerz9:~/Workspaces/Seafile$ docker compose up -d
[+] Running 38/38
 ✔ seafile Pulled                                                                                                                                 15.4s
   ✔ 505b3596871d Pull complete                                                                                                                    5.5s
   ✔ c083fef1fc73 Pull complete                                                                                                                    5.5s
   ✔ be8b84cba34b Pull complete                                                                                                                    6.5s
   ✔ 8bedc8e9293a Pull complete                                                                                                                    6.5s
   ✔ 622aaad4b3db Pull complete                                                                                                                    6.9s
   ✔ b873e65fb3d4 Pull complete                                                                                                                    7.7s
   ✔ 1d2e2524dcdd Pull complete                                                                                                                    9.1s
   ✔ 601ae82f9133 Pull complete                                                                                                                   10.4s
   ✔ 35a091cd3e29 Pull complete                                                                                                                   12.1s
   ✔ fadc521a35fb Pull complete                                                                                                                   12.1s
   ✔ 55d2f36eefb8 Pull complete                                                                                                                   12.1s
   ✔ b9383a1b21d0 Pull complete                                                                                                                   12.2s
   ✔ c18edc61dfc9 Pull complete                                                                                                                   12.2s
   ✔ 8ee4a48da408 Pull complete                                                                                                                   14.3s
 ✔ cache Pulled                                                                                                                                    5.2s
   ✔ 51dec6de03b1 Pull complete                                                                                                                    3.9s
   ✔ e6a929ea558d Pull complete                                                                                                                    3.9s
   ✔ b8f4f23b254e Pull complete                                                                                                                    4.1s
   ✔ 6ae38152dcb8 Pull complete                                                                                                                    4.1s
   ✔ e839fccf1879 Pull complete                                                                                                                    4.2s
 ✔ redis Pulled                                                                                                                                    5.2s
   ✔ ec781dee3f47 Pull complete                                                                                                                    3.9s
   ✔ 5f7274725e4f Pull complete                                                                                                                    3.9s
   ✔ f4f2f7018ed9 Pull complete                                                                                                                    3.9s
   ✔ 3f63903b0cb8 Pull complete                                                                                                                    4.1s
   ✔ c9ff57cee690 Pull complete                                                                                                                    4.1s
   ✔ 4f4fb700ef54 Pull complete                                                                                                                    4.1s
   ✔ 3e6b2202a764 Pull complete                                                                                                                    4.2s
 ✔ db Pulled                                                                                                                                       4.1s
   ✔ 817807f3c64e Pull complete                                                                                                                    1.4s
   ✔ 5843c951070e Pull complete                                                                                                                    1.4s
   ✔ 61a0e6ba478b Pull complete                                                                                                                    1.6s
   ✔ c688148c30c1 Pull complete                                                                                                                    1.6s
   ✔ 86447b1c7e20 Pull complete                                                                                                                    1.6s
   ✔ 943dc8eb543e Pull complete                                                                                                                    3.0s
   ✔ af136b8199a6 Pull complete                                                                                                                    3.0s
   ✔ 7761d3ebebab Pull complete                                                                                                                    3.0s
[+] Running 5/5
 ✔ Network seafile_default  Created                                                                                                                0.0s
 ✔ Container Seafile-CACHE  Started                                                                                                                0.4s
 ✔ Container Seafile-REDIS  Started                                                                                                                0.4s
 ✔ Container Seafile-DB     Started                                                                                                                0.4s
 ✔ Container Seafile        Started                                                                                                                0.3s

</code></pre></div></div>

<p>Again, I found uploading small files under 1Mb seemed to be fine, but the moment we used something larger like a 3Mb jpg, it fell down</p>

<p><a href="/content/images/2026/03/seafile-17.png"><img src="/content/images/2026/03/seafile-17.png" alt="/content/images/2026/03/seafile-17.png" /></a></p>

<h2 id="wikis">wikis</h2>

<p>Let’s try some of the other features, perhaps they’ll work better.  We can go to Wikis to create a new Wiki</p>

<p><a href="/content/images/2026/03/seafile-18.png"><img src="/content/images/2026/03/seafile-18.png" alt="/content/images/2026/03/seafile-18.png" /></a></p>

<p>Once created, we can click on the wiki to see some basic nav and page create</p>

<p><a href="/content/images/2026/03/seafile-19.png"><img src="/content/images/2026/03/seafile-19.png" alt="/content/images/2026/03/seafile-19.png" /></a></p>

<p>New Page, however, just gave me a spinning icon and error</p>

<p><a href="/content/images/2026/03/seafile-20.png"><img src="/content/images/2026/03/seafile-20.png" alt="/content/images/2026/03/seafile-20.png" /></a></p>

<h2 id="fat-client">Fat Client</h2>

<p>I’ll try the fat client on windows</p>

<p><a href="/content/images/2026/03/seafile-21.png"><img src="/content/images/2026/03/seafile-21.png" alt="/content/images/2026/03/seafile-21.png" /></a></p>

<p>I could then login</p>

<p><a href="/content/images/2026/03/seafile-22.png"><img src="/content/images/2026/03/seafile-22.png" alt="/content/images/2026/03/seafile-22.png" /></a></p>

<p>and it prompted me to sync files</p>

<p><a href="/content/images/2026/03/seafile-23.png"><img src="/content/images/2026/03/seafile-23.png" alt="/content/images/2026/03/seafile-23.png" /></a></p>

<p>and…. error</p>

<p><a href="/content/images/2026/03/seafile-24.png"><img src="/content/images/2026/03/seafile-24.png" alt="/content/images/2026/03/seafile-24.png" /></a></p>

<p>My next thought was perhaps I needed a new container.  Sometimes we use older images in writeups and it just needs something a tad newer.</p>

<p>However, the latest container is 13.0-latest</p>

<p><a href="/content/images/2026/03/seafile-25.png"><img src="/content/images/2026/03/seafile-25.png" alt="/content/images/2026/03/seafile-25.png" /></a></p>

<h2 id="kubernetes-native">Kubernetes native</h2>

<p>They seemed to strip out their setup docs from versions 12 and 13, but I found a K8s guide for 11 <a href="https://manual.seafile.com/11.0/deploy/deploy_with_k8s/">here</a></p>

<p>I was losing patience so I just asked Gemini to build the single manifest</p>

<p><a href="/content/images/2026/03/seafile-26.png"><img src="/content/images/2026/03/seafile-26.png" alt="/content/images/2026/03/seafile-26.png" /></a></p>

<p>I tweaked a few settings including using HTTPS and the newer 13 image.  Also, because we would be creating a memcache and mysql database, i felt we should do this in a namespace, so I created a “seafile” namespace to launch it all in</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~$ kubectl create ns seafile
namespace/seafile created
builder@DESKTOP-QADGF36:~$ cat ./seafile.ingress.yaml
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: azuredns-tpkpw
    ingress.kubernetes.io/ssl-redirect: "true"
    kubernetes.io/tls-acme: "true"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.org/websocket-services: seafile
  generation: 1
  name: seafileingress
spec:
  ingressClassName: nginx
  rules:
  - host: seafile.tpk.pw
    http:
      paths:
      - backend:
          service:
            name: seafile
            port:
              number: 80
        path: /
        pathType: ImplementationSpecific
  tls:
  - hosts:
    - seafile.tpk.pw
    secretName: seafile-tls
---
# --- MariaDB Storage ---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: mariadb-data
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: /opt/seafile-mysql/db
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mariadb-data
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
---
# --- MariaDB Deployment &amp; Service ---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mariadb
spec:
  selector:
    matchLabels:
      app: mariadb
  replicas: 1
  template:
    metadata:
      labels:
        app: mariadb
    spec:
      containers:
      - name: mariadb
        image: mariadb:10.11
        env:
        - name: MARIADB_ROOT_PASSWORD
          value: "db_dev"
        - name: MARIADB_AUTO_UPGRADE
          value: "true"
        ports:
        - containerPort: 3306
        volumeMounts:
        - name: mariadb-data
          mountPath: /var/lib/mysql
      volumes:
      - name: mariadb-data
        persistentVolumeClaim:
          claimName: mariadb-data
---
apiVersion: v1
kind: Service
metadata:
  name: mariadb
spec:
  selector:
    app: mariadb
  ports:
  - protocol: TCP
    port: 3306
    targetPort: 3306
---
# --- Memcached Deployment &amp; Service ---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: memcached
spec:
  replicas: 1
  selector:
    matchLabels:
      app: memcached
  template:
    metadata:
      labels:
        app: memcached
    spec:
      containers:
      - name: memcached
        image: memcached:1.6.18
        args: ["-m", "256"]
        ports:
        - containerPort: 11211
---
apiVersion: v1
kind: Service
metadata:
  name: memcached
spec:
  selector:
    app: memcached
  ports:
  - protocol: TCP
    port: 11211
    targetPort: 11211
---
# --- Seafile Storage ---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: seafile-data
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: /opt/seafile-data
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: seafile-data
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
---
# --- Seafile Deployment &amp; Service ---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: seafile
spec:
  replicas: 1
  selector:
    matchLabels:
      app: seafile
  template:
    metadata:
      labels:
        app: seafile
    spec:
      imagePullSecrets:
      - name: regcred
      containers:
      - name: seafile
        image: seafileltd/seafile-mc:13.0-latest
        env:
        - name: DB_HOST
          value: "mariadb"
        - name: DB_ROOT_PASSWD
          value: "db_dev"
        - name: TIME_ZONE
          value: "America/Chicago"
        - name: SEAFILE_ADMIN_EMAIL
          value: "isaac.johnson@gmail.com"
        - name: SEAFILE_ADMIN_PASSWORD
          value: "notmypassword!"
        - name: SEAFILE_SERVER_LETSENCRYPT
          value: "false"
        - name: SEAFILE_SERVER_HOSTNAME
          value: "seafile.tpk.pw"
        - name: SEAFILE_SERVER_PROTOCOL
          value: "https"
        volumeMounts:
        - name: seafile-data
          mountPath: /shared
      volumes:
      - name: seafile-data
        persistentVolumeClaim:
          claimName: seafile-data
      restartPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
  name: seafile
spec:
  selector:
    app: seafile
  type: ClusterIP
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
    name: seafile
builder@DESKTOP-QADGF36:~$ kubectl apply -f ./seafile.ingress.yaml -n seafile
ingress.networking.k8s.io/seafileingress created
persistentvolume/mariadb-data created
persistentvolumeclaim/mariadb-data created
deployment.apps/mariadb created
service/mariadb created
deployment.apps/memcached created
service/memcached created
persistentvolume/seafile-data created
persistentvolumeclaim/seafile-data created
deployment.apps/seafile created
service/seafile created
</code></pre></div></div>

<p>The TLS was satisfied quickly</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get cert -n seafile
NAME          READY   SECRET        AGE
seafile-tls   True    seafile-tls   115s
</code></pre></div></div>

<p>I got a bad gateway and even tested with a port-forward</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl port-forward svc/seafile 8088:80 -n seafile
Forwarding from 127.0.0.1:8088 -&gt; 80
Forwarding from [::1]:8088 -&gt; 80
Handling connection for 8088
</code></pre></div></div>

<p><a href="/content/images/2026/03/seafile-27.png"><img src="/content/images/2026/03/seafile-27.png" alt="/content/images/2026/03/seafile-27.png" /></a></p>

<p>I suspect that the newer 13 image uses 8611 for ingress wheres the older used 80.</p>

<p>I updated the service, but then the port-forward failed again</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl port-forward svc/seafile 8088:80 -n seafile
Forwarding from 127.0.0.1:8088 -&gt; 8611
Forwarding from [::1]:8088 -&gt; 8611
Handling connection for 8088
Handling connection for 8088
E0330 07:16:09.747304   92389 portforward.go:424] "Unhandled Error" err="an error occurred forwarding 8088 -&gt; 8611: error forwarding port 8611 to pod c1aa362dd7787302b716ebed56bda756d463d23766877717f5eabb2d59a4ea4b, uid : failed to execute portforward in network namespace \"/var/run/netns/cni-9f586fd4-dd56-1ac4-2bc0-a5b1d3c74659\": failed to connect to localhost:8611 inside namespace \"c1aa362dd7787302b716ebed56bda756d463d23766877717f5eabb2d59a4ea4b\", IPv4: dial tcp4 127.0.0.1:8611: connect: connection refused IPv6 dial tcp6: address localhost: no suitable address found "
error: lost connection to pod
</code></pre></div></div>

<p>The pod seems confused about the MariaDB (MySQL)</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl logs seafile-75c9d8ccbb-bp5xk  -n seafile
*** Running /etc/my_init.d/01_create_data_links.sh...
*** Booting runit daemon...
*** Runit started as PID 11
*** Running /scripts/enterpoint.sh...
2026-03-30 07:10:19 Nginx ready
2026-03-30 07:10:19 This is an idle script (infinite loop) to keep container running.
nginx: [warn] conflicting server name "" on 0.0.0.0:80, ignored
nginx: [warn] conflicting server name "" on 0.0.0.0:80, ignored
waiting for mysql server to be ready: mysql is not ready
waiting for mysql server to be ready: mysql is not ready
waiting for mysql server to be ready: mysql is not ready
waiting for mysql server to be ready: mysql is not ready
</code></pre></div></div>

<p>I kept banging away at it, adding the missing Redis, setting MysQL ports</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">networking.k8s.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Ingress</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">annotations</span><span class="pi">:</span>
    <span class="na">cert-manager.io/cluster-issuer</span><span class="pi">:</span> <span class="s">azuredns-tpkpw</span>
    <span class="na">ingress.kubernetes.io/ssl-redirect</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
    <span class="na">kubernetes.io/tls-acme</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
    <span class="na">nginx.ingress.kubernetes.io/proxy-read-timeout</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3600"</span>
    <span class="na">nginx.ingress.kubernetes.io/proxy-send-timeout</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3600"</span>
    <span class="na">nginx.org/websocket-services</span><span class="pi">:</span> <span class="s">seafile</span>
  <span class="na">generation</span><span class="pi">:</span> <span class="m">1</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">seafileingress</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">ingressClassName</span><span class="pi">:</span> <span class="s">nginx</span>
  <span class="na">rules</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">host</span><span class="pi">:</span> <span class="s">seafile.tpk.pw</span>
    <span class="na">http</span><span class="pi">:</span>
      <span class="na">paths</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">backend</span><span class="pi">:</span>
          <span class="na">service</span><span class="pi">:</span>
            <span class="na">name</span><span class="pi">:</span> <span class="s">seafile</span>
            <span class="na">port</span><span class="pi">:</span>
              <span class="na">number</span><span class="pi">:</span> <span class="m">80</span>
        <span class="na">path</span><span class="pi">:</span> <span class="s">/</span>
        <span class="na">pathType</span><span class="pi">:</span> <span class="s">ImplementationSpecific</span>
  <span class="na">tls</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">hosts</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">seafile.tpk.pw</span>
    <span class="na">secretName</span><span class="pi">:</span> <span class="s">seafile-tls</span>
<span class="nn">---</span>
<span class="c1"># --- MariaDB Storage ---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">PersistentVolume</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">mariadb-data</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">capacity</span><span class="pi">:</span>
    <span class="na">storage</span><span class="pi">:</span> <span class="s">1Gi</span>
  <span class="na">accessModes</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">ReadWriteOnce</span>
  <span class="na">hostPath</span><span class="pi">:</span>
    <span class="na">path</span><span class="pi">:</span> <span class="s">/opt/seafile-mysql/db</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">PersistentVolumeClaim</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">mariadb-data</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">accessModes</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">ReadWriteOnce</span>
  <span class="na">resources</span><span class="pi">:</span>
    <span class="na">requests</span><span class="pi">:</span>
      <span class="na">storage</span><span class="pi">:</span> <span class="s">10Gi</span>
<span class="nn">---</span>
<span class="c1"># --- MariaDB Deployment &amp; Service ---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">mariadb</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">mariadb</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">1</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app</span><span class="pi">:</span> <span class="s">mariadb</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">mariadb</span>
        <span class="na">image</span><span class="pi">:</span> <span class="s">mariadb:10.11</span>
        <span class="na">env</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">MARIADB_ROOT_PASSWORD</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">db_dev"</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">MARIADB_AUTO_UPGRADE</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">MARIADB_DATABASE</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">seafile_db"</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">MARIADB_USER</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">seafileuser"</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">MARIADB_PASSWORD</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">seafilepassword"</span>
        <span class="na">ports</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">containerPort</span><span class="pi">:</span> <span class="m">3306</span>
        <span class="na">volumeMounts</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">mariadb-data</span>
          <span class="na">mountPath</span><span class="pi">:</span> <span class="s">/var/lib/mysql</span>
      <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">mariadb-data</span>
        <span class="na">persistentVolumeClaim</span><span class="pi">:</span>
          <span class="na">claimName</span><span class="pi">:</span> <span class="s">mariadb-data</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">mariadb</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">mariadb</span>
  <span class="na">ports</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">protocol</span><span class="pi">:</span> <span class="s">TCP</span>
    <span class="na">port</span><span class="pi">:</span> <span class="m">3306</span>
    <span class="na">targetPort</span><span class="pi">:</span> <span class="m">3306</span>
<span class="nn">---</span>
<span class="c1"># --- Memcached Deployment &amp; Service ---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">memcached</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">1</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">memcached</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app</span><span class="pi">:</span> <span class="s">memcached</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">memcached</span>
        <span class="na">image</span><span class="pi">:</span> <span class="s">memcached:1.6.18</span>
        <span class="na">args</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">-m"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">256"</span><span class="pi">]</span>
        <span class="na">ports</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">containerPort</span><span class="pi">:</span> <span class="m">11211</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">memcached</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">memcached</span>
  <span class="na">ports</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">protocol</span><span class="pi">:</span> <span class="s">TCP</span>
    <span class="na">port</span><span class="pi">:</span> <span class="m">11211</span>
    <span class="na">targetPort</span><span class="pi">:</span> <span class="m">11211</span>
<span class="nn">---</span>
<span class="c1"># --- Redis Deployment &amp; Service ---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">redis</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">1</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">redis</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app</span><span class="pi">:</span> <span class="s">redis</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">redis</span>
        <span class="na">image</span><span class="pi">:</span> <span class="s">redis:latest</span>
        <span class="na">command</span><span class="pi">:</span> 
          <span class="pi">-</span> <span class="s">redis-server</span>
          <span class="pi">-</span> <span class="s2">"</span><span class="s">--requirepass"</span>
          <span class="pi">-</span> <span class="s2">"</span><span class="s">redispass"</span>
        <span class="na">ports</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">containerPort</span><span class="pi">:</span> <span class="m">6379</span>
        <span class="na">volumeMounts</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">redis-data</span>
          <span class="na">mountPath</span><span class="pi">:</span> <span class="s">/data</span>
      <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">redis-data</span>
        <span class="na">emptyDir</span><span class="pi">:</span> <span class="pi">{}</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">redis</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">redis</span>
  <span class="na">ports</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">protocol</span><span class="pi">:</span> <span class="s">TCP</span>
    <span class="na">port</span><span class="pi">:</span> <span class="m">6379</span>
    <span class="na">targetPort</span><span class="pi">:</span> <span class="m">6379</span>
<span class="nn">---</span>
<span class="c1"># --- Seafile Storage ---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">PersistentVolume</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">seafile-data</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">capacity</span><span class="pi">:</span>
    <span class="na">storage</span><span class="pi">:</span> <span class="s">10Gi</span>
  <span class="na">accessModes</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">ReadWriteOnce</span>
  <span class="na">hostPath</span><span class="pi">:</span>
    <span class="na">path</span><span class="pi">:</span> <span class="s">/opt/seafile-data</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">PersistentVolumeClaim</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">seafile-data</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">accessModes</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">ReadWriteOnce</span>
  <span class="na">resources</span><span class="pi">:</span>
    <span class="na">requests</span><span class="pi">:</span>
      <span class="na">storage</span><span class="pi">:</span> <span class="s">10Gi</span>
<span class="nn">---</span>
<span class="c1"># --- Seafile Deployment &amp; Service ---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">seafile</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">1</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">seafile</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app</span><span class="pi">:</span> <span class="s">seafile</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">imagePullSecrets</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">regcred</span>
      <span class="na">containers</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">seafile</span>
        <span class="na">image</span><span class="pi">:</span> <span class="s">seafileltd/seafile-mc:13.0-latest</span>
        <span class="na">env</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">INIT_SEAFILE_MYSQL_ROOT_PASSWORD</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">db_dev"</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">SEAFILE_MYSQL_DB_HOST</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">mariadb"</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">SEAFILE_MYSQL_DB_USER</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">seafileuser"</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">SEAFILE_MYSQL_DB_PASSWORD</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">seafilepassword"</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">SEAFILE_MYSQL_DB_PORT</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3306"</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">SEAFILE_MYSQL_DB_SEAFILE_DB_NAME</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">seafile_db"</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">SEAFILE_MYSQL_DB_CCNET_DB_NAME</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">ccnet_db"</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">SEAFILE_MYSQL_DB_SEAHUB_DB_NAME</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">seahub_db"</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">CACHE_PROVIDER</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">redis"</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">REDIS_HOST</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">redis"</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">REDIS_PORT</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">6379"</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">REDIS_PASSWORD</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">redispass"</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">TIME_ZONE</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">America/Chicago"</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">SEAFILE_VOLUME</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/opt/seafile-data"</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">SEAFILE_MYSQL_VOLUME</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/opt/seafile-mysql/db"</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">INIT_SEAFILE_ADMIN_EMAIL</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">isaac.johnson@gmail.com"</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">INIT_SEAFILE_ADMIN_PASSWORD</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">notmypassword!"</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">JWT_PRIVATE_KEY</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">dOxZYTTZgXKMHkqLBIQVImayQXAVWdzGBPuFJKggzcgvgPJPXpWzqzKaUOIOGGIr"</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">SEADOC_VOLUME</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/opt/seadoc-data"</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">SEADOC_IMAGE</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">seafileltd/sdoc-server:2.0-latest"</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">ENABLE_SEADOC</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">false"</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">SEADOC_SERVER_URL</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https://seafile.tpk.pw/sdoc-server"</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">SEAFILE_SERVER_HOSTNAME</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">seafile.tpk.pw"</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">SEAFILE_SERVER_PROTOCOL</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https"</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">FORCE_HTTPS_IN_CONF</span>
          <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
        <span class="na">volumeMounts</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">seafile-data</span>
          <span class="na">mountPath</span><span class="pi">:</span> <span class="s">/shared</span>
      <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">seafile-data</span>
        <span class="na">persistentVolumeClaim</span><span class="pi">:</span>
          <span class="na">claimName</span><span class="pi">:</span> <span class="s">seafile-data</span>
      <span class="na">restartPolicy</span><span class="pi">:</span> <span class="s">Always</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">seafile</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">seafile</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">ClusterIP</span>
  <span class="na">ports</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">protocol</span><span class="pi">:</span> <span class="s">TCP</span>
    <span class="na">port</span><span class="pi">:</span> <span class="m">80</span>
    <span class="na">targetPort</span><span class="pi">:</span> <span class="m">8611</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">seafile</span>
</code></pre></div></div>

<p>apply as I went</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl apply -f ./seafile.ingress.yaml -n seafile
ingress.networking.k8s.io/seafileingress unchanged
persistentvolume/mariadb-data unchanged
persistentvolumeclaim/mariadb-data unchanged
deployment.apps/mariadb unchanged
service/mariadb unchanged
deployment.apps/memcached unchanged
service/memcached unchanged
deployment.apps/redis created
service/redis created
persistentvolume/seafile-data unchanged
persistentvolumeclaim/seafile-data unchanged
deployment.apps/seafile configured
service/seafile unchanged
</code></pre></div></div>

<p>And after changing the service targetPort from 8611 back to 80</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>---
apiVersion: v1
kind: Service
metadata:
  name: seafile
spec:
  selector:
    app: seafile
  type: ClusterIP
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
    name: seafile
</code></pre></div></div>

<p>it finally worked</p>

<p><a href="/content/images/2026/03/seafile-28.png"><img src="/content/images/2026/03/seafile-28.png" alt="/content/images/2026/03/seafile-28.png" /></a></p>

<p>But just as before, it failed to upload any file of even moderate size</p>

<p><a href="/content/images/2026/03/seafile-29.png"><img src="/content/images/2026/03/seafile-29.png" alt="/content/images/2026/03/seafile-29.png" /></a></p>

<p>and there are no crashes nor anything in the logs</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/Seafile$ kubectl get po -n seafile
NAME                        READY   STATUS    RESTARTS   AGE
mariadb-86dbcfffb-hhr8h     1/1     Running   0          42m
memcached-d45ccfcd7-qjgwv   1/1     Running   0          42m
redis-5bb476f74b-pqfmq      1/1     Running   0          34m
seafile-5b4dbf44b6-7b28j    1/1     Running   0          34m
builder@DESKTOP-QADGF36:~/Workspaces/Seafile$ kubectl logs seafile-5b4dbf44b6-7b28j -n seafile
*** Running /etc/my_init.d/01_create_data_links.sh...
*** Booting runit daemon...
*** Runit started as PID 14
*** Running /scripts/enterpoint.sh...
2026-03-30 07:32:04 Waiting Nginx
nginx: [warn] conflicting server name "" on 0.0.0.0:80, ignored
2026-03-30 07:32:04 Nginx ready
2026-03-30 07:32:04 This is an idle script (infinite loop) to keep container running.
nginx: [warn] conflicting server name "" on 0.0.0.0:80, ignored
[2026-03-30 07:32:04] Skip running setup-seafile-mysql.py because there is existing seafile-data folder.
[03/30/2026 07:32:04][upgrade]: The container was recreated, start fix the media symlinks
mv: not replacing '/shared/seafile/seahub-data/avatars/default-non-register.jpg'
mv: not replacing '/shared/seafile/seahub-data/avatars/default.png'
mv: not replacing '/shared/seafile/seahub-data/avatars/groups'
[03/30/2026 07:32:04][upgrade]: Done

Starting seafile server, please wait ...
Seafile server started

Done.

Starting seahub at port 8000 ...



----------------------------------------
Successfully created seafile admin
----------------------------------------




Seahub is started

Done.
</code></pre></div></div>

<p>I think at this point I’m done with Seafile</p>

<h2 id="saas">SaaS</h2>

<p>There is a hosted edition one can use</p>

<p><a href="/content/images/2026/03/seafile-30.png"><img src="/content/images/2026/03/seafile-30.png" alt="/content/images/2026/03/seafile-30.png" /></a></p>

<p>You can store at most 1Gb, but it’s <a href="https://plus.seafile.com/org/register/">easy to register</a>.</p>

<p>That said, I don’t think I’ll pursue the SaaS for now</p>

<h1 id="summary">Summary</h1>

<p><a href="https://github.com/Panonim/dynacat">Dynacat</a> was easy to install and use.  Much like its upstream <a href="https://github.com/glanceapp/glance">glance</a>, it does a good job of creating a landing page.  It was easy to self host at <a href="https://dynacat.tpk.pw/">https://dynacat.tpk.pw/</a>.  I tweaked it a bit for some YT feeds, stocks and services.  I may circle back and do more but in truth, I’m not much of a landing page person.</p>

<p><a href="https://www.seafile.com/">Seafile</a> started okay, but quickly fell down on any file of size.  I tried many different options - from Docker to Kubernetes.  The company itself is based in China and Singapore and has <a href="https://www.seafile.com/en/about/">been around since 2012</a> so perhaps there are just bugs with this latest version as it’s less than a month old.</p>]]></content><author><name>Isaac Johnson</name></author><category term="opensource" /><category term="Dynacat" /><category term="glance" /><category term="Seafile" /><category term="docker" /><category term="containers" /><category term="kubernetes" /><summary type="html"><![CDATA[A while back I saw this Marius post about Dynacat, an interesting fork of Glances that looks to be more self-contained.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://freshbrewed.science/content/images/2026/03/Isaac_Johnson_a_purple_cat_on_an_orange_beach_at_sunrise_--ar_8003290b-5aa2-4595-ab25-336bd2de587f_0.png" /><media:content medium="image" url="https://freshbrewed.science/content/images/2026/03/Isaac_Johnson_a_purple_cat_on_an_orange_beach_at_sunrise_--ar_8003290b-5aa2-4595-ab25-336bd2de587f_0.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Coder in Docker and K8s</title><link href="https://freshbrewed.science/2026/03/31/coder.html" rel="alternate" type="text/html" title="Coder in Docker and K8s" /><published>2026-03-31T10:00:01+00:00</published><updated>2026-03-31T10:00:01+00:00</updated><id>https://freshbrewed.science/2026/03/31/coder</id><content type="html" xml:base="https://freshbrewed.science/2026/03/31/coder.html"><![CDATA[<p>I’ve been running <a href="https://github.com/coder/coder">Coder</a> and <a href="https://github.com/coder/code-server">Code-server</a> for quite some time now.  I generally operate locally, but there are times that having a good remote coding solution based on VS Code is quite handy.</p>

<p>Just last year after GCP Next 25, I <a href="https://freshbrewed.science/2025/04/14/coder.html">updated and wrote about Coder and Code-server</a> then around Christmas, revisited <a href="https://freshbrewed.science/2025/12/25/maint.html">Code-server updates</a>.</p>

<h1 id="upgrading">Upgrading</h1>

<p>Again, I see there are updates</p>

<p><a href="/content/images/2026/03/coder-01.png"><img src="/content/images/2026/03/coder-01.png" alt="/content/images/2026/03/coder-01.png" /></a></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ helm repo update
$ helm upgrade coder coder-v2/coder --namespace coder --version 2.30.5
coalesce.go:298: warning: cannot overwrite table with non table for coder.coder.ingress.tls (map[enable:false secretName: wildcardSecretName:])
coalesce.go:298: warning: cannot overwrite table with non table for coder.coder.ingress.tls (map[enable:false secretName: wildcardSecretName:])
Release "coder" has been upgraded. Happy Helming!
NAME: coder
LAST DEPLOYED: Fri Mar 27 07:30:26 2026
NAMESPACE: coder
STATUS: deployed
REVISION: 4
TEST SUITE: None
NOTES:
Enjoy Coder! Please create an issue at https://github.com/coder/coder if you run
into any problems! :)
</code></pre></div></div>

<h2 id="cleanup">Cleanup</h2>

<p>Any workspace one doesn’t need can just be cleaned up with “Delete”.</p>

<p><a href="/content/images/2026/03/coder-02.png"><img src="/content/images/2026/03/coder-02.png" alt="/content/images/2026/03/coder-02.png" /></a></p>

<p>which, after confirming, basically does a terraform destroy on the resource</p>

<p><a href="/content/images/2026/03/coder-03.png"><img src="/content/images/2026/03/coder-03.png" alt="/content/images/2026/03/coder-03.png" /></a></p>

<p>Lest you think it failed, it just takes a minute to cleanup all the resources before briefly showing “deleting”</p>

<p><a href="/content/images/2026/03/coder-04.png"><img src="/content/images/2026/03/coder-04.png" alt="/content/images/2026/03/coder-04.png" /></a></p>

<h2 id="launching-existing-workspace">Launching existing Workspace</h2>

<p>I’ll go to the one from 3 months ago which is a very large git repo and click start</p>

<p><a href="/content/images/2026/03/coder-05.png"><img src="/content/images/2026/03/coder-05.png" alt="/content/images/2026/03/coder-05.png" /></a></p>

<p>Terraform then kicks in</p>

<p><a href="/content/images/2026/03/coder-06.png"><img src="/content/images/2026/03/coder-06.png" alt="/content/images/2026/03/coder-06.png" /></a></p>

<p>before showing me a placeholder screen as it launches</p>

<p><a href="/content/images/2026/03/coder-07.png"><img src="/content/images/2026/03/coder-07.png" alt="/content/images/2026/03/coder-07.png" /></a></p>

<p>In just a moment it was launched again and I could see usage details</p>

<p><a href="/content/images/2026/03/coder-08.png"><img src="/content/images/2026/03/coder-08.png" alt="/content/images/2026/03/coder-08.png" /></a></p>

<p>But it just kept crashing</p>

<p><a href="/content/images/2026/03/coder-09.png"><img src="/content/images/2026/03/coder-09.png" alt="/content/images/2026/03/coder-09.png" /></a></p>

<p>The logs suggested connectivity issues.</p>

<p>I see the pod up</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get po -n coder
NAME                                                          READY   STATUS    RESTARTS   AGE
coder-5b5e3c82-075c-4243-983b-aff287de7ee9-55b865c49f-qc6zd   1/1     Running   0          6m18s
coder-6cd484f7f8-xqrzp                                        1/1     Running   0          11m
coder-db-postgresql-0                                         1/1     Running   0          94d
</code></pre></div></div>

<p>But the container keeps spitting logs</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl logs coder-5b5e3c82-075c-4243-983b-aff287de7ee9-55b865c49f-qc6zd -n coder
Error from server: Get "https://192.168.1.214:10250/containerLogs/coder/coder-5b5e3c82-075c-4243-983b-aff287de7ee9-55b865c49f-qc6zd/dev": net/http: TLS handshake timeout
$ kubectl logs coder-5b5e3c82-075c-4243-983b-aff287de7ee9-55b865c49f-qc6zd -n coder
Error from server: Get "https://192.168.1.214:10250/containerLogs/coder/coder-5b5e3c82-075c-4243-983b-aff287de7ee9-55b865c49f-qc6zd/dev": proxy error from 127.0.0.1:6443 while dialing 192.168.1.214:10250, code 502: 502 Bad Gateway
</code></pre></div></div>

<p>and the events suggest it keeps killing the container in the pod</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
$ kubectl describe po coder-5b5e3c82-075c-4243-983b-aff287de7ee9-55b865c49f-qc6zd -n coder | tail -n 9
Events:
  Type     Reason        Age                  From               Message
  ----     ------        ----                 ----               -------
  Normal   Scheduled     6m59s                default-scheduler  Successfully assigned coder/coder-5b5e3c82-075c-4243-983b-aff287de7ee9-55b865c49f-qc6zd to builder-hp-elitebook-850-g2
  Normal   Pulling       6m58s                kubelet            Pulling image "codercom/enterprise-base:ubuntu"
  Normal   Pulled        6m29s                kubelet            Successfully pulled image "codercom/enterprise-base:ubuntu" in 28.592s (28.592s including waiting). Image size: 376791434 bytes.
  Normal   Created       6m29s                kubelet            Created container: dev
  Normal   Started       6m29s                kubelet            Started container dev
  Warning  NodeNotReady  88s (x2 over 3m55s)  node-controller    Node is not ready
</code></pre></div></div>

<p>This just kept failing me.</p>

<p>I tried to make a new workspace but it just kept signing me out</p>

<p><a href="/content/images/2026/03/coder-10.png"><img src="/content/images/2026/03/coder-10.png" alt="/content/images/2026/03/coder-10.png" /></a></p>

<p>I came back after a day (as the cluster control plane was misbehaving and I didn’t want to assume this was Coder’s fault)</p>

<p>I fired up a new workspace</p>

<p><a href="/content/images/2026/03/coder-29.png"><img src="/content/images/2026/03/coder-29.png" alt="/content/images/2026/03/coder-29.png" /></a></p>

<p>While Github has now blocked the Github Copilot chat client</p>

<video muted="" controls="">
    <source src="/content/images/2026/03/coder-29.mp4" type="video/mp4" />
</video>

<p>But we can use Google Code Assist (GCA) to quickly build and test a basic python app</p>

<video muted="" controls="">
    <source src="/content/images/2026/03/coder-30.mp4" type="video/mp4" />
</video>

<p>Lastly, we can create a project in <a href="https://gitlab.com">Gitlab</a> and push these changes up for sharing</p>

<video muted="" controls="">
    <source src="/content/images/2026/03/coder-31.mp4" type="video/mp4" />
</video>

<p>Which you can now <a href="https://gitlab.com/isaac.johnson/pythonsampleapp/-/tree/main?ref_type=heads">check out here</a></p>

<h2 id="linux">Linux</h2>

<p>let’s try just reducing some complexity and use the <a href="https://coder.com/docs/install">install script</a> to run locally in Ubuntu</p>

<p>I initially had some failures</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@bosgamerz7:~$ sudo systemctl enable --now coder
Created symlink /etc/systemd/system/multi-user.target.wants/coder.service → /lib/systemd/system/coder.service.
Job for coder.service failed because the control process exited with error code.
See "systemctl status coder.service" and "journalctl -xeu coder.service" for details.
</code></pre></div></div>

<p>This is because coder really wants to use port 3000 and that was used by Openweb-UI already</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@bosgamerz7:~$ docker ps
CONTAINER ID   IMAGE                                COMMAND                  CREATED       STATUS                 PORTS                                                        NAMES
a5e0885202bd   ghcr.io/open-webui/open-webui:main   "bash start.sh"          3 weeks ago   Up 3 weeks (healthy)   0.0.0.0:3000-&gt;8080/tcp, [::]:3000-&gt;8080/tcp                  open-webui
bb0f798b1570   nicolargo/glances:latest             "/bin/sh -c '/venv/b…"   5 weeks ago   Up 3 weeks             0.0.0.0:61208-&gt;61208/tcp, [::]:61208-&gt;61208/tcp, 61209/tcp   glances
</code></pre></div></div>

<p>I just needed to stop it and start the server again</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@bosgamerz7:~$ docker stop open-webui
open-webui
builder@bosgamerz7:~$ sudo systemctl enable --now coder
builder@bosgamerz7:~$ journalctl -u coder.service -b
</code></pre></div></div>

<p>The logs showed a reverse proxy was setup which rather surprised me</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Mar 27 08:19:27 bosgamerz7 systemd[1]: coder.service: Main process exited, code=exited, status=1/FAILURE
Mar 27 08:19:27 bosgamerz7 systemd[1]: coder.service: Failed with result 'exit-code'.
Mar 27 08:19:27 bosgamerz7 systemd[1]: Failed to start "Coder - Self-hosted developer workspaces on your infra".
Mar 27 08:19:32 bosgamerz7 systemd[1]: coder.service: Scheduled restart job, restart counter is at 17.
Mar 27 08:19:32 bosgamerz7 systemd[1]: Stopped "Coder - Self-hosted developer workspaces on your infra".
Mar 27 08:19:32 bosgamerz7 systemd[1]: Starting "Coder - Self-hosted developer workspaces on your infra"...
Mar 27 08:19:32 bosgamerz7 coder[2794252]: Started HTTP listener at http://127.0.0.1:3000
Mar 27 08:19:32 bosgamerz7 coder[2794252]: Using built-in PostgreSQL (/home/coder/.config/coderv2/postgres)
Mar 27 08:19:35 bosgamerz7 coder[2794252]: Opening tunnel so workspaces can connect to your deployment. For production scenarios, specify an external access URL
Mar 27 08:19:39 bosgamerz7 coder[2794252]: Using tunnel in US East Pittsburgh with latency 48.107215ms.
Mar 27 08:19:39 bosgamerz7 coder[2794252]: ╔═══════════════════════════════════════════════╗
Mar 27 08:19:39 bosgamerz7 coder[2794252]: ║               View the Web UI:                ║
Mar 27 08:19:39 bosgamerz7 coder[2794252]: ║   https://jb1ec3dknpbj4.pit-1.try.coder.app   ║
Mar 27 08:19:39 bosgamerz7 coder[2794252]: ╚═══════════════════════════════════════════════╝
Mar 27 08:19:45 bosgamerz7 coder[2794252]: 2026-03-27 13:19:45.172 [info]  coderd: injecting default github external auth provider  type=github  client_id=Iv1.6a2b4b4ae&gt;
Mar 27 08:19:45 bosgamerz7 coder[2794252]: ==&gt; Logs will stream in below (press ctrl+c to gracefully exit):
Mar 27 08:19:45 bosgamerz7 coder[2794252]: 2026-03-27 13:19:45.173 [info]  notifications.manager.notifier: started  notifier_id=7ff018a4-ae01-45cd-bb9c-fbaf29af80a0
Mar 27 08:19:45 bosgamerz7 coder[2794252]: 2026-03-27 13:19:45.176 [info]  notifications.report_generator: report generator is executing the job for the first time  not&gt;
Mar 27 08:19:45 bosgamerz7 coder[2794252]: 2026-03-27 13:19:45.176 [info]  notifications.report_generator: report generator finished  duration=3.079398ms
Mar 27 08:19:46 bosgamerz7 systemd[1]: Started "Coder - Self-hosted developer workspaces on your infra".
</code></pre></div></div>

<p>I was able to login</p>

<p><a href="/content/images/2026/03/coder-11.png"><img src="/content/images/2026/03/coder-11.png" alt="/content/images/2026/03/coder-11.png" /></a></p>

<h3 id="templates">Templates</h3>
<p>I’ll first try a DinD template</p>

<p><a href="/content/images/2026/03/coder-12.png"><img src="/content/images/2026/03/coder-12.png" alt="/content/images/2026/03/coder-12.png" /></a></p>

<p>I was fine with the defaults</p>

<p><a href="/content/images/2026/03/coder-13.png"><img src="/content/images/2026/03/coder-13.png" alt="/content/images/2026/03/coder-13.png" /></a></p>

<p>which then launched the template</p>

<p><a href="/content/images/2026/03/coder-14.png"><img src="/content/images/2026/03/coder-14.png" alt="/content/images/2026/03/coder-14.png" /></a></p>

<p>We can then “create workspace”</p>

<p><a href="/content/images/2026/03/coder-15.png"><img src="/content/images/2026/03/coder-15.png" alt="/content/images/2026/03/coder-15.png" /></a></p>

<p>I’m not sure what will happen, but I’ll try adding a private git repo (it does have a devcontainer.json, but it’s not public and I see nowhere to add GIT credentials)</p>

<p><a href="/content/images/2026/03/coder-16.png"><img src="/content/images/2026/03/coder-16.png" alt="/content/images/2026/03/coder-16.png" /></a></p>

<p>I can see a container was fired up on the host</p>

<p><a href="/content/images/2026/03/coder-17.png"><img src="/content/images/2026/03/coder-17.png" alt="/content/images/2026/03/coder-17.png" /></a></p>

<p>I didn’t see it right away, but in the startup logs it asked to setup a Code app to see my repo</p>

<p><a href="/content/images/2026/03/coder-18.png"><img src="/content/images/2026/03/coder-18.png" alt="/content/images/2026/03/coder-18.png" /></a></p>

<p>But even restarting failed to work and there really isn’t any details on how to sort this out.</p>

<p><a href="/content/images/2026/03/coder-19.png"><img src="/content/images/2026/03/coder-19.png" alt="/content/images/2026/03/coder-19.png" /></a></p>

<p>While the DinD didn’t give me a code server, i tried adding the Docker Container one (not Docker in Docker), then using that for a new workspace</p>

<p><a href="/content/images/2026/03/coder-20.png"><img src="/content/images/2026/03/coder-20.png" alt="/content/images/2026/03/coder-20.png" /></a></p>

<p>That fired up with a Code server.  I can see we can use Github Copilot</p>

<p><a href="/content/images/2026/03/coder-22.png"><img src="/content/images/2026/03/coder-22.png" alt="/content/images/2026/03/coder-22.png" /></a></p>

<p>But we can also use local models with the Continue.dev plugin</p>

<p><a href="/content/images/2026/03/coder-24.png"><img src="/content/images/2026/03/coder-24.png" alt="/content/images/2026/03/coder-24.png" /></a></p>

<p>I’ll add an embedded, autocomplete and two chat models. Note, they use different servers in my network so i need to set the apiBase as well</p>

<p><a href="/content/images/2026/03/coder-25.png"><img src="/content/images/2026/03/coder-25.png" alt="/content/images/2026/03/coder-25.png" /></a></p>

<p>Here we can see it in action</p>

<video muted="" controls="">
    <source src="/content/images/2026/03/coder-26.mp4" type="video/mp4" />
</video>

<p>Hopefully one can appreciate what is going on above - I am using a reverse proxy tunnel to a shuttle Linux host that is running coder.</p>

<p>The coder workspace is then using a Continue.dev plugin to reach Ollama - both on that same host as well as a different Ollama host in my network.</p>

<p>This basically means I can have a Coding self hosted system entirely self contained.</p>

<p>When adding in a Gitea or Forgejo repo, we can even host our GIT code in-network safely as well (<em>safely is a relative term…  For my important code, i replicate between Forgejo and Gitea and for my most important code, I also sync it up to Codeberg</em>)</p>

<video muted="" controls="">
    <source src="/content/images/2026/03/coder-27.mp4" type="video/mp4" />
</video>

<p>And for reference, in both <a href="https://forgejo.org/">Forgejo</a> and <a href="https://about.gitea.com/">Gitea</a>, those mirror settings are in main settings for each Repo</p>

<p><a href="/content/images/2026/03/coder-28.png"><img src="/content/images/2026/03/coder-28.png" alt="/content/images/2026/03/coder-28.png" /></a></p>

<h1 id="summary">Summary</h1>

<p>Today we circled back on <a href="https://github.com/coder/coder">Coder</a> and upgrading <a href="https://coder.tpk.pw/">my local Kubernetes based instance</a> with helm.  This initially didn’t go well, but what was really going on was my primary control plane server was acting up and ultimately I needed to reboot it.  After a reboot, most of my services in Kubernetes started to behave again including Coder.</p>

<p>I fired up a containerized K8s-based workspace and initially tried to get Github Copilot to work (but as it won’t install properly anymore in codeserver instances, it was a no-go).  I pivoted to Google Code Assist (GCA) which worked just fine to build a hello world python app, show it running then push it to a <a href="https://gitlab.com/isaac.johnson/pythonsampleapp/-/tree/main?ref_type=heads">fresh repo in Gitlab</a>.</p>

<p>I also explored running Coder natively on a Linux host.  When one does it this way, there is a reverse proxy created on coder.app.  We then connected there to create a new user and attempted to Auth to Github, but could not solve private repo access that way.  However, we easily setup Docker-in-Docker as well Docker based containers using the Continue.dev plug for LLM usage against local infrastructure.  Lastly, we saved our code out in a private <a href="https://forgejo.freshbrewed.science/">Forgejo</a> repository.</p>

<p>While I didn’t need to use VPN access as the LLM host was in the same 192.168 address space, this diagram covers the general setup</p>

<p><a href="/content/images/2026/03/coder-32.png"><img src="/content/images/2026/03/coder-32.png" alt="/content/images/2026/03/coder-32.png" /></a></p>

<p>This means we have the best of both worlds - some use of SaaS services like Gitlab, Github and GCA, but by no means are we required to use them as we can host our GIT with Gitea/Forgejo just as easily.  And while Cloud hosted LLMs have more speed and power than a shuttle PC in my network, using smaller 8b models does work at the cost of speed.</p>]]></content><author><name>Isaac Johnson</name></author><category term="GenAI" /><category term="LLM" /><category term="Gemini" /><category term="Pi" /><category term="Linux" /><category term="Ollama" /><summary type="html"><![CDATA[I’ve been running Coder and Code-server for quite some time now. I generally operate locally, but there are times that having a good remote coding solution based on VS Code is quite handy.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://freshbrewed.science/content/images/2026/03/coderbg.png" /><media:content medium="image" url="https://freshbrewed.science/content/images/2026/03/coderbg.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Pi Coding Agent</title><link href="https://freshbrewed.science/2026/03/26/piagent.html" rel="alternate" type="text/html" title="Pi Coding Agent" /><published>2026-03-26T10:00:01+00:00</published><updated>2026-03-26T10:00:01+00:00</updated><id>https://freshbrewed.science/2026/03/26/piagent</id><content type="html" xml:base="https://freshbrewed.science/2026/03/26/piagent.html"><![CDATA[<p>I came across a YT video extolling the virtues of <a href="https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent">Pi CLI</a>.  While yes, it is another CLI, it has a very light footprint and just enough skills (like command line access) to do things.</p>

<p>We’ll test it on a few different hosts and models; both cloud and local. After some basic tests (involving good old <a href="https://en.wikipedia.org/wiki/Commander_Keen">Commander Keen</a>), we’ll use Pi + Gemini to build a Pomodoro app.</p>

<h1 id="installing-pi">Installing Pi</h1>

<p>Like many other tools we can install <code class="language-plaintext highlighter-rouge">npm</code></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(base) builder@LuiGi:~/Workspaces/piagent$ npm install -g @mariozechner/pi-coding-agent
npm warn deprecated node-domexception@1.0.0: Use your platform's native DOMException instead

added 257 packages in 21s

34 packages are looking for funding
  run `npm fund` for details
(base) builder@LuiGi:~/Workspaces/piagent$
</code></pre></div></div>

<p>Now I just need to fire <code class="language-plaintext highlighter-rouge">pi</code> to launch</p>

<p><a href="/content/images/2026/03/pi-01.png"><img src="/content/images/2026/03/pi-01.png" alt="/content/images/2026/03/pi-01.png" /></a></p>

<p>I gave a quick test to ask for Commander Keen as ASCII art but it just gave me a penguin afaik</p>

<p><a href="/content/images/2026/03/pi-02.png"><img src="/content/images/2026/03/pi-02.png" alt="/content/images/2026/03/pi-02.png" /></a></p>

<p>It claims to have spent 12.5c on that</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~/Workspaces/piagent
↑77k ↓1.6k R37k $0.124 0.8%/1.0M (auto)                                                     gemini-2.5-pro • medium
</code></pre></div></div>

<p>If I search models, we only see Google ones.  This is because it noticed I only had a GEMINI API key set in my env vars so it just assumed I would use Google</p>

<p><a href="/content/images/2026/03/pi-03.png"><img src="/content/images/2026/03/pi-03.png" alt="/content/images/2026/03/pi-03.png" /></a></p>

<p>Next, I tried setting an OPENAI KEY and BASE to match my GPT 5 Nano deployment in Azure AI Foundry.</p>

<p><a href="/content/images/2026/03/pi-04.png"><img src="/content/images/2026/03/pi-04.png" alt="/content/images/2026/03/pi-04.png" /></a></p>

<p>However, as we can see, it doesn’t respect the env var for OPENAI_API_BASE to a cognativeservices URL.</p>

<p>However, once I switched to a proper OPENAI API Key, then it worked just fine</p>

<p><a href="/content/images/2026/03/pi-05.png"><img src="/content/images/2026/03/pi-05.png" alt="/content/images/2026/03/pi-05.png" /></a></p>

<p>We can see it picked up my existing agent skills</p>

<p><a href="/content/images/2026/03/pi-06.png"><img src="/content/images/2026/03/pi-06.png" alt="/content/images/2026/03/pi-06.png" /></a></p>

<p>Revisiting <a href="https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/providers.md">the providers docs</a>, I think I need to use their format for the Azure OpenAI instances</p>

<p>Let’s set those</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>export AZURE_OPENAI_API_KEY=BgxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxiC
export AZURE_OPENAI_BASE_URL=https://isaac-mgp1gfv5-eastus2.cognitiveservices.azure.com
export AZURE_OPENAI_DEPLOYMENT=gpt-5-nano
export AZURE_OPENAI_API_VERSION=2024-12-01-preview
</code></pre></div></div>

<p>Though trying a few permutations for resource name, nothing seemed to work on GPT 5 via Azure AI Foundry</p>

<p><a href="/content/images/2026/03/pi-07.png"><img src="/content/images/2026/03/pi-07.png" alt="/content/images/2026/03/pi-07.png" /></a></p>

<p>But we can see it works fine with Gemini Flash</p>

<p><a href="/content/images/2026/03/pi-08.png"><img src="/content/images/2026/03/pi-08.png" alt="/content/images/2026/03/pi-08.png" /></a></p>

<p>Let’s compare to Gemini CLI</p>

<p><a href="/content/images/2026/03/pi-09.png"><img src="/content/images/2026/03/pi-09.png" alt="/content/images/2026/03/pi-09.png" /></a></p>

<p>I thought it was interesting that this used 17k tokens to make a cow</p>

<p><a href="/content/images/2026/03/pi-10.png"><img src="/content/images/2026/03/pi-10.png" alt="/content/images/2026/03/pi-10.png" /></a></p>

<p>Whereas Pi used just over 7k.  Just to be sure of this, I tried again</p>

<p><a href="/content/images/2026/03/pi-11.png"><img src="/content/images/2026/03/pi-11.png" alt="/content/images/2026/03/pi-11.png" /></a></p>

<p>and indeed it was about 4.5k used.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~/Workspaces/piagent
↑4.5k ↓63 R2.0k $0.003 0.2%/1.0M (auto)                                    (google) gemini-3-flash-preview • medium
</code></pre></div></div>

<h2 id="custom-models-and-ollama">Custom Models and Ollama</h2>

<p>Right now we have no custom models</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
cat: /home/builder/.pi/agent/models.json: No such file or directory (os error 2)
</code></pre></div></div>

<p>example Ollama</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
  "providers": {
    "ollama": {
      "baseUrl": "http://localhost:11434/v1",
      "api": "openai-completions",
      "apiKey": "ollama",
      "models": [
        { "id": "llama3.1:8b" },
        { "id": "qwen2.5-coder:7b" }
      ]
    }
  }
}
</code></pre></div></div>

<p>So, when in my home network, I could use</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(base) builder@LuiGi:~/Workspaces$ cd piagent/
(base) builder@LuiGi:~/Workspaces/piagent$ ls
 commander_keen.png   cow.txt        'search?q=commander+keen&amp;tbm=isch'
 commander_keen.txt   cow_ascii.txt   search_results.html
(base) builder@LuiGi:~/Workspaces/piagent$ cat ~/.pi/agent/
auth.json      bin/           models.json    sessions/      settings.json  skills/
(base) builder@LuiGi:~/Workspaces/piagent$ cat ~/.pi/agent/models.json

{
  "providers": {
    "ollama": {
      "baseUrl": "http://192.168.1.143:11434/v1",
      "api": "openai-completions",
      "apiKey": "ollama",
      "models": [
        { "id": "gemma3:4b" },
        { "id": "qwen3:8b" },
        { "id": "qwen2.5-coder:1.5b" },
        { "id": "llama3.1:8b" },
        { "id": "llama3.2:3b" },
        { "id": "deepseek-r1:7b" }
      ]
    }
  }
}
</code></pre></div></div>

<p>I tried asking gemma3 but it didn’t support tools.  I then asked qwen3:8b and it just went out to lunch</p>

<p><a href="/content/images/2026/03/pi-13.png"><img src="/content/images/2026/03/pi-13.png" alt="/content/images/2026/03/pi-13.png" /></a></p>

<p>However, qwen2.5-coder worked</p>

<p><a href="/content/images/2026/03/pi-14.png"><img src="/content/images/2026/03/pi-14.png" alt="/content/images/2026/03/pi-14.png" /></a></p>

<p>I did find llama3.1:8b, while slow, did a pretty decent job</p>

<p><a href="/content/images/2026/03/pi-15.png"><img src="/content/images/2026/03/pi-15.png" alt="/content/images/2026/03/pi-15.png" /></a></p>

<p>Those were using an Ollama on a dedicated host in network.  I was curious how well a laptop with no real video card would handle an 8b</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(base) builder@LuiGi:~/Workspaces/piagent$ ollama pull llama3.1:8b
pulling manifest
pulling 667b0c1932bc: 100% ▕█████████████████████████████████████████████████████▏ 4.9 GB
pulling 948af2743fc7: 100% ▕█████████████████████████████████████████████████████▏ 1.5 KB
pulling 0ba8f0e314b4: 100% ▕█████████████████████████████████████████████████████▏  12 KB
pulling 56bb8bd477a5: 100% ▕█████████████████████████████████████████████████████▏   96 B
pulling 455f34728c9b: 100% ▕█████████████████████████████████████████████████████▏  487 B
verifying sha256 digest
writing manifest
success
</code></pre></div></div>

<p>Then I switched up my models to use localhost</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(base) builder@LuiGi:~/Workspaces/piagent$ vi ~/.pi/agent/models.json
(base) builder@LuiGi:~/Workspaces/piagent$ cat ~/.pi/agent/models.json

{
  "providers": {
    "ollama": {
      "baseUrl": "http://localhost:11434/v1",
      "api": "openai-completions",
      "apiKey": "ollama",
      "models": [
        { "id": "llama3.1:8b" }
      ]
    }
  }
}
</code></pre></div></div>

<p>It ran, and after about 10 minutes did display something</p>

<p><a href="/content/images/2026/03/pi-16.png"><img src="/content/images/2026/03/pi-16.png" alt="/content/images/2026/03/pi-16.png" /></a></p>

<p>I tried a simple python app request</p>

<p><a href="/content/images/2026/03/pi-17.png"><img src="/content/images/2026/03/pi-17.png" alt="/content/images/2026/03/pi-17.png" /></a></p>

<p>Again, on the LG Gram laptop it was slow, but it did work.</p>

<p>I’ll try on my laptop with a proper GPU</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@builder-Lenny16:~/Workspaces/pitest$ cat ~/.pi/agent/models.json
{
  "providers": {
    "ollama": {
      "baseUrl": "http://localhost:11434/v1",
      "api": "openai-completions",
      "apiKey": "ollama",
      "models": [
        { "id": "mistral-nemo:12b-instruct-2407-q4_K_M" },
        { "id": "qwen2.5-coder:14b" },
        { "id": "gemma3:12b" },
        { "id": "deepseek-r1:14b" },
        { "id": "qwen3:14b" }
    }
  }
}
</code></pre></div></div>

<p>I did record it building an app, but it really failed at writing files the first time.  Then got hung up on a python library that doesn’t exist.</p>

<p><a href="/content/images/2026/03/pi-18.png"><img src="/content/images/2026/03/pi-18.png" alt="/content/images/2026/03/pi-18.png" /></a></p>

<p>Here you can see it with a local 14b model improve the UI.</p>

<video muted="" controls="">
    <source src="/content/images/2026/03/pi-19.mp4" type="video/mp4" />
</video>

<p><em>note: it did leak the API key in the video above so I expired it right after</em></p>

<h2 id="mixed-use">Mixed use</h2>

<p>I had the idea of mixing things up.  What if I built out the basic app with local models and Pi, then pivoted to Gemini CLI with Stitch to make it ‘pretty’.</p>

<p>I think that would be the most optimized on token use.</p>

<p>I stewed a bit on an idea before coming up with one.</p>

<p>The used to be an Adobe Air app, pomodorio that was this very clean minimalist UI that was reminiscent of WinAmp.  I found it very useful.  Too many pomodoro apps over complicate things or take the full screen.  I need small.</p>

<p>To get this done though, I have a bit of housecleaning.  I need to stash my skills, now that I’m actually using them in my regular flow.</p>

<p>I’ll create a private repo in my own Git system (Forgejo)</p>

<p><a href="/content/images/2026/03/pi-20.png"><img src="/content/images/2026/03/pi-20.png" alt="/content/images/2026/03/pi-20.png" /></a></p>

<p>Part of my flow is to let an admin create repos and then invite my lower privileged user to collaborate</p>

<p><a href="/content/images/2026/03/pi-21.png"><img src="/content/images/2026/03/pi-21.png" alt="/content/images/2026/03/pi-21.png" /></a></p>

<p>I’m going to try and do this in a plan to plan to do approach.</p>

<p>So that means making an initial plan:</p>
<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh">$ cat plan.md
---
</span>name: implementation-planner
description: Creates detailed implementation plans for new features.
prompt: You are an expert software planner. Your task is to generate a comprehensive plan in markdown format.
tools:
<span class="p">  -</span> name: FileSystem
<span class="gh">    actions: [read, search]
---
</span>
<span class="gh"># Plan Request</span>

This app should create a 25 minute timer with 5 minute rest period in the form of the Pomodoro technique.

Required features:
<span class="p">-</span> light/dark mode
<span class="p">-</span> settings with
<span class="p">  -</span> changes to default work/rest times
<span class="p">  -</span> ability to set notification sound (or disable)
<span class="p">  -</span> size (scale with font and UI from 50% to 200% size).

This should build self contained with a dockerfile.

This app should allow download and upload of planned tasks and track "pom"s on each task completed.  we can mark tasks closed.  The input and output should be in a JSON format.

<span class="gh"># required outputs</span>

Dockerfile for the app
Helm chart to install the app with
<span class="p">-</span> deployment
<span class="p">-</span> service
<span class="p">-</span> option ingress with annotations

<span class="gu">## UI</span>

The UI should be simple and angular based on the original WinAmp style (https://en.wikipedia.org/wiki/Winamp)

We should have a few options for color themes
</code></pre></div></div>

<p>I fired it up and it went really quite fast</p>

<video muted="" controls="">
    <source src="/content/images/2026/03/pi-22.mp4" type="video/mp4" />
</video>

<p>I saw it made some form of an app, tests and dockerfile, but no evidence of a helm chart</p>

<p><a href="/content/images/2026/03/pi-23.png"><img src="/content/images/2026/03/pi-23.png" alt="/content/images/2026/03/pi-23.png" /></a></p>

<p>It did more on a second pass, but still left some things out</p>

<p><a href="/content/images/2026/03/pi-24.png"><img src="/content/images/2026/03/pi-24.png" alt="/content/images/2026/03/pi-24.png" /></a></p>

<p>I spent another 5 minutes in Gemini CLI letting it do cleanup and work which didn’t use that many tokens</p>

<p><a href="/content/images/2026/03/pi-25.png"><img src="/content/images/2026/03/pi-25.png" alt="/content/images/2026/03/pi-25.png" /></a></p>

<p>I continued to test and make minor improvements with Gemini - adding a proper whip sound then fixing the helm chart</p>

<p><a href="/content/images/2026/03/pi-26.png"><img src="/content/images/2026/03/pi-26.png" alt="/content/images/2026/03/pi-26.png" /></a></p>

<h2 id="launching-in-kubernetes">Launching in Kubernetes</h2>

<p>Now that I have an app, I want to actually test it in my hosted environment.</p>

<p>I need a quick A record first</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ az account set --subscription "Pay-As-You-Go" &amp;&amp; az network dns record-set a add-record -g idjdnsrg -z tpk.pw -a 76.156.69.232 -n pomo
{
  "ARecords": [
    {
      "ipv4Address": "76.156.69.232"
    }
  ],
  "TTL": 3600,
  "etag": "1aec837a-3d39-47c8-929d-756b65cbedaa",
  "fqdn": "pomo.tpk.pw.",
  "id": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/idjdnsrg/providers/Microsoft.Network/dnszones/tpk.pw/A/pomo",
  "name": "pomo",
  "provisioningState": "Succeeded",
  "resourceGroup": "idjdnsrg",
  "targetResource": {},
  "trafficManagementProfile": {},
  "type": "Microsoft.Network/dnszones/A"
}
</code></pre></div></div>

<p>I pushed <a href="https://hub.docker.com/repository/docker/idjohnson/pomodoro-app/general">my container to Dockerhub</a></p>

<p><a href="/content/images/2026/03/pi-27.png"><img src="/content/images/2026/03/pi-27.png" alt="/content/images/2026/03/pi-27.png" /></a></p>

<p>and after a quick helm deploy, I had a pretty good functional app</p>

<p><a href="/content/images/2026/03/pi-28.png"><img src="/content/images/2026/03/pi-28.png" alt="/content/images/2026/03/pi-28.png" /></a></p>

<p>You are welcome to use the app as it’s now hosted at <a href="https://pomo.tpk.pw/">pomo.tpk.pw</a>.</p>

<p>Also, because caring is sharing, I put all the code into Github: <a href="https://github.com/idjohnson/pomoApp">https://github.com/idjohnson/pomoApp</a>.</p>

<h1 id="summary">Summary</h1>

<p>Thus far Pi looks like a pretty good local tool.  I get annoyed that it sometimes fails to write files, but seems to do fine after the files are initially laid down.</p>

<p>I found it’s pretty performant on decent hardware.  Yes, that is like saying sports cars go fast, but it’s worth noting that on my Lenovo Legion with a 12gb 5070 it runs smooth, but on the LG Gram with just CPU its a good 10+ minute wait.</p>

<p>This isn’t a deal breaker, however, there are times I’m just happy to ask the LLM to do some work and then set the laptop down and watch a show or get back to other work.</p>

<p>I want to explore pushing the limits of tools next.  As we know, the downside to local models is they get out of date and have fixed knowledge.  Making sure they can reach out to the interwebs and fetch latest data is key to really making them useful.</p>

<p>However, I plan to definitely keep Pi in my stack as writing tools or coming up with ideas when I’m either offline or in low internet areas is quite useful.</p>]]></content><author><name>Isaac Johnson</name></author><category term="GenAI" /><category term="LLM" /><category term="Gemini" /><category term="Pi" /><category term="Linux" /><category term="Ollama" /><summary type="html"><![CDATA[I came across a YT video extolling the virtues of Pi CLI. While yes, it is another CLI, it has a very light footprint and just enough skills (like command line access) to do things.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://freshbrewed.science/content/images/2026/03/Gemini_Generated_Image_bnf1e3bnf1e3bnf1.png" /><media:content medium="image" url="https://freshbrewed.science/content/images/2026/03/Gemini_Generated_Image_bnf1e3bnf1e3bnf1.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Homelab tweaks: Kubeconfigs and ext IPs</title><link href="https://freshbrewed.science/2026/03/24/homelabtlc.html" rel="alternate" type="text/html" title="Homelab tweaks: Kubeconfigs and ext IPs" /><published>2026-03-24T10:00:01+00:00</published><updated>2026-03-24T10:00:01+00:00</updated><id>https://freshbrewed.science/2026/03/24/homelabtlc</id><content type="html" xml:base="https://freshbrewed.science/2026/03/24/homelabtlc.html"><![CDATA[<p>One of my challenges is that with rapid experimentation, especially in blasting and redoing my varied Kubernetes clusters, the management of my Kubernetes config files on all my laptops was getting a bit challenging.</p>

<p>A while back I had <a href="https://github.com/idjohnson/ansible-playbooks/blob/main/updateKConfigs.pl">created a perl script</a> that used to work but frankly it fell down long ago and mangled the shared kubeconfig file.</p>

<p>I thought now might be a good time to both come up with a scalable solution to my hosts (using Ansible) and also notify me when my egress IP changes (as then I have to update my configs for a new IP).</p>

<h1 id="a-better-k8s-config">A Better K8s Config</h1>

<p>One of my challenges is that over time, my big kubeconfig gets to be a bit much.</p>

<p>My original design was to store a base64’ed version in AKV then pull it down on any laptop I might be using to access all my Kubernetes.</p>

<p>Something like</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ getpass.sh k3sremoteconfig idjakv | base64 --decode | tee ~/.kube/config
</code></pre></div></div>

<p>which really just does</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ az keyvault secret show --vault-name idjakv --name k3sremoteconfig --subscription Pay-As-You-Go | jq -r .value
</code></pre></div></div>

<p><em>note: you can find those scripts (setpass.sh, getpass.sh and listpass.sh) <a href="https://freshbrewed.science/2023/10/24/workitem-revisit.html">here</a></em></p>

<p>However, the combined config got too big for AKV and in the end, it’s mangled and gone</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces$ getpass.sh k3sremoteconfig idjakv


</code></pre></div></div>

<h2 id="storing-a-different-way">Storing a different way</h2>

<p>So I rethought it - really I just need the configs from primary and secondary.</p>

<p>This seems like a good job for a playbook with a script</p>

<p>First, I make a working script - this will find the hostid (4th part of an IPv4 address) then pull the k3s config and store it in AKV</p>

<p>Here is <a href="https://github.com/idjohnson/ansible-playbooks/blob/main/setk8s.sh">setk8s.sh</a></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>

<span class="c"># find my local IP</span>
<span class="nb">export </span><span class="nv">MYIP</span><span class="o">=</span><span class="sb">`</span>ifconfig | <span class="nb">grep </span>192.168.1 | <span class="nb">head</span> <span class="nt">-n1</span> | <span class="nb">sed</span> <span class="s1">'s/^.*inet \(.*\)  netmask.*/\1/'</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>
<span class="nb">export </span><span class="nv">MYHOST</span><span class="o">=</span><span class="sb">`</span>ifconfig | <span class="nb">grep </span>192.168.1 | <span class="nb">head</span> <span class="nt">-n1</span> | <span class="nb">sed</span> <span class="s1">'s/^.*inet 192.168.1.\(.*\)  netmask.*/\1/'</span> |
 <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>

<span class="c"># create local int</span>
<span class="nb">cat</span> /etc/rancher/k3s/k3s.yaml | <span class="nb">sed</span> <span class="s2">"s/127.0.0.1/</span><span class="nv">$MYIP</span><span class="s2">/g"</span> | <span class="nb">tee</span> /tmp/k8s-int

<span class="c"># save in AKV</span>
az keyvault secret <span class="nb">set</span> <span class="nt">--vault-name</span> idjakv <span class="nt">--name</span> int<span class="nv">$MYHOST</span><span class="nt">-int</span> <span class="nt">--file</span> /tmp/k8s-int
</code></pre></div></div>

<p>Then I need <a href="https://github.com/idjohnson/ansible-playbooks/blob/main/setk8s.yaml">the playbook</a> that will call it for my hosts</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Save AKV K8s Int</span>
  <span class="na">hosts</span><span class="pi">:</span> <span class="s">all</span>

  <span class="na">tasks</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Transfer the script</span>
    <span class="na">copy</span><span class="pi">:</span> <span class="s">src=setk8s.sh dest=/tmp mode=0755</span>

  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">SaveThisK8sInt</span>
    <span class="na">command</span><span class="pi">:</span> <span class="s">sh /tmp/setk8s.sh</span>
    <span class="na">args</span><span class="pi">:</span>
      <span class="na">chdir</span><span class="pi">:</span> <span class="s">/tmp</span>
</code></pre></div></div>

<p>Now I could just onboard this job to AWX and call it for each host (primary and secondary control-plane hosts), however it would be best as a general turnkey playbook so I made an inventory of just Kubernetes control-plane (master) hosts.</p>

<p><a href="/content/images/2026/03/ansible-01.png"><img src="/content/images/2026/03/ansible-01.png" alt="/content/images/2026/03/ansible-01.png" /></a></p>

<p>This might require re-authing to Azure</p>

<video muted="" controls="">
    <source src="/content/images/2026/03/ansible-02.mp4" type="video/mp4" />
</video>

<p>I could do better with my naming, but i know what it means. I can now see the two creds in AKV</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ listpass.sh idjakv | grep int
int247-int                                 https://idjakv.vault.azure.net/secrets/int247-int                                                                                                                                                                                                                      True
int77-int                                  https://idjakv.vault.azure.net/secrets/int77-int                                                                                                                                                                                                                       True
</code></pre></div></div>

<p>My <a href="https://github.com/idjohnson/ansible-playbooks/blob/main/updateKConfigs.pl">old perl script</a> worked for a while, but had hard-coded ports and servers - not really ideal.</p>

<p>This new setup doesn’t b64 the k8s configs, rather stores them as files as it’s just the single ones.</p>

<p><a href="/content/images/2026/03/ansible-03.png"><img src="/content/images/2026/03/ansible-03.png" alt="/content/images/2026/03/ansible-03.png" /></a></p>

<h2 id="not-ai">Not AI</h2>

<p>So I don’t want to let my brain atrophy further by just letting some LLM write the code.  I want to make this.  I’ll use an LLM just if I get really stuck.</p>

<p>My first thought is to use <code class="language-plaintext highlighter-rouge">yq</code> to replace values.</p>

<p>I can fetch the old certificate authority data for both the “int” (internal URL) and “ext” (external URL) using ‘mac77’ and ‘ext77’.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cat ~/.kube/config | yq '.clusters[] | select (.name == "mac77") | .cluster."certificate-authority
-data"'
"TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdCwgc2VkIGRvIGVpdXNtb2QgdGVtcG9yIGluY2lkaWR1bnQgdXQgbGFib3JlIGV0IGRvbG9yZSBtYWduYSBhbGlxdWEuIFV0IGVuaW0gYWQgbWluaW0gdmVuaWFtLCBxdWlzIG5vc3RydWQgZXhlcmNpdGF0aW9uIHVsbGFtY28gbGFib3JpcyBuaXNpIHV0IGFsaXF1aXAgZXggZWEgY29tbW9kbyBjb25zZXF1YXQuIER1aXMgYXV0ZSBpcnVyZSBkb2xvciBpbiByZXByZWhlbmRlcml0IGluIHZvbHVwdGF0ZSB2ZWxpdCBlc3NlIGNpbGx1bSBkb2xvcmUgZXUgZnVnaWF0IG51bGxhIHBhcmlhdHVyLiBFeGNlcHRldXIgc2ludCBvY2NhZWNhdCBjdXBpZGF0YXQgbm9uIHByb2lkZW50LCBzdW50IGluIGN1bHBhIHF1aSBvZmZpY2lhIGRlc2VydW50IG1vbGxpdCBhbmltIGlkIGVzdCBsYWJvcnVtCg=="
</code></pre></div></div>

<p>This might be even easier than I assumed.</p>

<p>I’ll set a variable to the old value</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ export OLDINT=`cat ~/.kube/config | yq -r '.clusters[] | select (.name == "ext77") | .cluster."certificate-authority-data"'`
</code></pre></div></div>

<p>Then one to my new value</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ export NEWINT=`getpass.sh int77-int idjakv | yq  -r '.clusters[0].cluster."certificate-authority-data"'`
</code></pre></div></div>

<p>I can now whip up a quick bash script to see if they are a match or different</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cat ./test.sh
#!/bin/bash

OLDCERTAUTHDATA=`cat ~/.kube/config | yq -r '.clusters[] | select (.name == "mac77") | .cluster."certificate-authority-data"' | tr -d '\n'`
NEWCERTAUTHDATA=`az keyvault secret show --vault-name idjakv --name int77-int --subscription Pay-As-You-Go | jq -r .value | yq  -r '.clusters[0].cluster."certificate-authority-data"' | tr -d '\n'`

if [[ "$OLDCERTAUTHDATA" == "$NEWCERTAUTHDATA" ]]
then
  echo match
else
  echo different
fi

$ ./test.sh
different
</code></pre></div></div>

<p>And if it’s been updated</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ./test.sh
match
</code></pre></div></div>

<p>Let’s now expand it to cover the user data as well</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cat ./test.sh
#!/bin/bash

# Current Mapping
SECONDARYCPIP=77

OLDCERTAUTHDATA=`cat ~/.kube/config | yq -r '.clusters[] | select (.name == "mac77") | .cluster."certificate-authority-data"' | tr -d '\n'`
NEWCERTAUTHDATA=`az keyvault secret show --vault-name idjakv --name int$SECONDARYCPIP-int --subscription Pay-As-You-Go | jq -r .value | yq  -r '.clusters[0].cluster."certificate-authority-data"' | tr -d '\n'`

if [[ "$OLDCERTAUTHDATA" == "$NEWCERTAUTHDATA" ]]
then
  echo "cert auth data match"
else
  echo "cert auth data different"
fi

# Users

OLDCLIENTCERTDATA=`cat ~/.kube/config | yq -r '.users[] | select (.name == "mac77") | .user."client-certificate-data"' | tr -d '\n'`
NEWCLIENTCERTDATA=`az keyvault secret show --vault-name idjakv --name int77-int --subscription Pay-As-You-Go | jq -r .value | yq -r '.users[0].user."client-certificate-data"' | tr -d '\n'`

OLDCLIENTKEYDATA=`cat ~/.kube/config | yq -r '.users[] | select (.name == "mac77") | .user."client-key-data"' | tr -d '\n'`
NEWCLIENTKEYDATA=`az keyvault secret show --vault-name idjakv --name int77-int --subscription Pay-As-You-Go | jq -r .value | yq -r '.users[0].user."client-key-data"' | tr -d '\n'`


if [[ "$OLDCLIENTCERTDATA" == "$NEWCLIENTCERTDATA" ]]
then
  echo "client cert data match"
else
  echo "client cert data different"
fi

if [[ "$OLDCLIENTKEYDATA" == "$NEWCLIENTKEYDATA" ]]
then
  echo "client key data match"
else
  echo "client key data different"
fi
</code></pre></div></div>

<p>Since I know my latest local config is up to date, I expect matches</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ./test.sh
cert auth data match
client cert data match
client key data match
</code></pre></div></div>

<p>My next step is to check both Internal and External blocks.  I also fixed some hard-coded variables above</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">cat</span> ./test.sh
<span class="c">#!/bin/bash</span>

<span class="c"># Current Mapping</span>
<span class="nv">SECONDARYCPIP</span><span class="o">=</span>77
<span class="nv">SECONDARYINTNAME</span><span class="o">=</span>mac77
<span class="nv">SECONDARYEXTNAME</span><span class="o">=</span>ext77

<span class="c">#####</span>
<span class="c"># Secondary Int</span>

<span class="nb">echo</span> <span class="s2">"==========================="</span>
<span class="nb">echo</span> <span class="s2">" Checking </span><span class="nv">$SECONDARYINTNAME</span><span class="s2"> "</span>
<span class="nb">echo</span> <span class="s2">"==========================="</span>

<span class="nv">OLDCERTAUTHDATA</span><span class="o">=</span><span class="sb">`</span><span class="nb">cat</span> ~/.kube/config | yq <span class="nt">-r</span> <span class="s2">".clusters[] | select (.name == </span><span class="se">\"</span><span class="nv">$SECONDARYINTNAME</span><span class="se">\"</span><span class="s2">) | .cluster.</span><span class="se">\"</span><span class="s2">certificate-authority-data</span><span class="se">\"</span><span class="s2">"</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>
<span class="nv">NEWCERTAUTHDATA</span><span class="o">=</span><span class="sb">`</span>az keyvault secret show <span class="nt">--vault-name</span> idjakv <span class="nt">--name</span> int<span class="nv">$SECONDARYCPIP</span><span class="nt">-int</span> <span class="nt">--subscription</span> Pay-As-You-Go | jq <span class="nt">-r</span> .value | yq  <span class="nt">-r</span> <span class="s1">'.clusters[0].cluster."certificate-authority-data"'</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>

<span class="k">if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$OLDCERTAUTHDATA</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"</span><span class="nv">$NEWCERTAUTHDATA</span><span class="s2">"</span> <span class="o">]]</span>
<span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"cert auth data match"</span>
<span class="k">else
  </span><span class="nb">echo</span> <span class="s2">"cert auth data different"</span>
<span class="k">fi</span>

<span class="c"># Users</span>

<span class="nv">OLDCLIENTCERTDATA</span><span class="o">=</span><span class="sb">`</span><span class="nb">cat</span> ~/.kube/config | yq <span class="nt">-r</span> <span class="s2">".users[] | select (.name == </span><span class="se">\"</span><span class="nv">$SECONDARYINTNAME</span><span class="se">\"</span><span class="s2">) | .user.</span><span class="se">\"</span><span class="s2">client-certificate-data</span><span class="se">\"</span><span class="s2">"</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>
<span class="nv">NEWCLIENTCERTDATA</span><span class="o">=</span><span class="sb">`</span>az keyvault secret show <span class="nt">--vault-name</span> idjakv <span class="nt">--name</span> int<span class="nv">$SECONDARYCPIP</span><span class="nt">-int</span> <span class="nt">--subscription</span> Pay-As-You-Go | jq <span class="nt">-r</span> .value | yq <span class="nt">-r</span> <span class="s1">'.users[0].user."client-certificate-data"'</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>

<span class="nv">OLDCLIENTKEYDATA</span><span class="o">=</span><span class="sb">`</span><span class="nb">cat</span> ~/.kube/config | yq <span class="nt">-r</span> <span class="s2">".users[] | select (.name == </span><span class="se">\"</span><span class="nv">$SECONDARYINTNAME</span><span class="se">\"</span><span class="s2">) | .user.</span><span class="se">\"</span><span class="s2">client-key-data</span><span class="se">\"</span><span class="s2">"</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>
<span class="nv">NEWCLIENTKEYDATA</span><span class="o">=</span><span class="sb">`</span>az keyvault secret show <span class="nt">--vault-name</span> idjakv <span class="nt">--name</span> int<span class="nv">$SECONDARYCPIP</span><span class="nt">-int</span> <span class="nt">--subscription</span> Pay-As-You-Go | jq <span class="nt">-r</span> .value | yq <span class="nt">-r</span> <span class="s1">'.users[0].user."client-key-data"'</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>


<span class="k">if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$OLDCLIENTCERTDATA</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"</span><span class="nv">$NEWCLIENTCERTDATA</span><span class="s2">"</span> <span class="o">]]</span>
<span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"client cert data match"</span>
<span class="k">else
  </span><span class="nb">echo</span> <span class="s2">"client cert data different"</span>
<span class="k">fi

if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$OLDCLIENTKEYDATA</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"</span><span class="nv">$NEWCLIENTKEYDATA</span><span class="s2">"</span> <span class="o">]]</span>
<span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"client key data match"</span>
<span class="k">else
  </span><span class="nb">echo</span> <span class="s2">"client key data different"</span>
<span class="k">fi</span>

<span class="c">#####</span>
<span class="c"># Secondary Int</span>

<span class="nb">echo</span> <span class="s2">"==========================="</span>
<span class="nb">echo</span> <span class="s2">" Checking </span><span class="nv">$SECONDARYEXTNAME</span><span class="s2"> "</span>
<span class="nb">echo</span> <span class="s2">"==========================="</span>

<span class="nv">OLDCERTAUTHDATA</span><span class="o">=</span><span class="sb">`</span><span class="nb">cat</span> ~/.kube/config | yq <span class="nt">-r</span> <span class="s2">".clusters[] | select (.name == </span><span class="se">\"</span><span class="nv">$SECONDARYEXTNAME</span><span class="se">\"</span><span class="s2">) | .cluster.</span><span class="se">\"</span><span class="s2">certificate-authority-data</span><span class="se">\"</span><span class="s2">"</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>
<span class="nv">NEWCERTAUTHDATA</span><span class="o">=</span><span class="sb">`</span>az keyvault secret show <span class="nt">--vault-name</span> idjakv <span class="nt">--name</span> int<span class="nv">$SECONDARYCPIP</span><span class="nt">-int</span> <span class="nt">--subscription</span> Pay-As-You-Go | jq <span class="nt">-r</span> .value | yq  <span class="nt">-r</span> <span class="s1">'.clusters[0].cluster."certificate-authority-data"'</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>

<span class="k">if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$OLDCERTAUTHDATA</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"</span><span class="nv">$NEWCERTAUTHDATA</span><span class="s2">"</span> <span class="o">]]</span>
<span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"cert auth data match"</span>
<span class="k">else
  </span><span class="nb">echo</span> <span class="s2">"cert auth data different"</span>
<span class="k">fi</span>

<span class="c"># Users</span>

<span class="nv">OLDCLIENTCERTDATA</span><span class="o">=</span><span class="sb">`</span><span class="nb">cat</span> ~/.kube/config | yq <span class="nt">-r</span> <span class="s2">".users[] | select (.name == </span><span class="se">\"</span><span class="nv">$SECONDARYEXTNAME</span><span class="se">\"</span><span class="s2">) | .user.</span><span class="se">\"</span><span class="s2">client-certificate-data</span><span class="se">\"</span><span class="s2">"</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>
<span class="nv">NEWCLIENTCERTDATA</span><span class="o">=</span><span class="sb">`</span>az keyvault secret show <span class="nt">--vault-name</span> idjakv <span class="nt">--name</span> int<span class="nv">$SECONDARYCPIP</span><span class="nt">-int</span> <span class="nt">--subscription</span> Pay-As-You-Go | jq <span class="nt">-r</span> .value | yq <span class="nt">-r</span> <span class="s1">'.users[0].user."client-certificate-data"'</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>

<span class="nv">OLDCLIENTKEYDATA</span><span class="o">=</span><span class="sb">`</span><span class="nb">cat</span> ~/.kube/config | yq <span class="nt">-r</span> <span class="s2">".users[] | select (.name == </span><span class="se">\"</span><span class="nv">$SECONDARYEXTNAME</span><span class="se">\"</span><span class="s2">) | .user.</span><span class="se">\"</span><span class="s2">client-key-data</span><span class="se">\"</span><span class="s2">"</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>
<span class="nv">NEWCLIENTKEYDATA</span><span class="o">=</span><span class="sb">`</span>az keyvault secret show <span class="nt">--vault-name</span> idjakv <span class="nt">--name</span> int<span class="nv">$SECONDARYCPIP</span><span class="nt">-int</span> <span class="nt">--subscription</span> Pay-As-You-Go | jq <span class="nt">-r</span> .value | yq <span class="nt">-r</span> <span class="s1">'.users[0].user."client-key-data"'</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>


<span class="k">if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$OLDCLIENTCERTDATA</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"</span><span class="nv">$NEWCLIENTCERTDATA</span><span class="s2">"</span> <span class="o">]]</span>
<span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"client cert data match"</span>
<span class="k">else
  </span><span class="nb">echo</span> <span class="s2">"client cert data different"</span>
<span class="k">fi

if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$OLDCLIENTKEYDATA</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"</span><span class="nv">$NEWCLIENTKEYDATA</span><span class="s2">"</span> <span class="o">]]</span>
<span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"client key data match"</span>
<span class="k">else
  </span><span class="nb">echo</span> <span class="s2">"client key data different"</span>
<span class="k">fi</span>
</code></pre></div></div>

<p>We can see our “ext” (external) block is in need of TLC</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ./test.sh
===========================
 Checking mac77
===========================
cert auth data match
client cert data match
client key data match
===========================
 Checking ext77
===========================
cert auth data different
client cert data different
client key data different
</code></pre></div></div>

<p>Next I added updates with a backup.  However, all the bash if else blocks are getting a bit long.  I might need to make some functions soon</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">cat</span> ./test.sh
<span class="c">#!/bin/bash</span>

<span class="c"># Current Mapping</span>
<span class="nv">SECONDARYCPIP</span><span class="o">=</span>77
<span class="nv">SECONDARYINTNAME</span><span class="o">=</span>mac77
<span class="nv">SECONDARYEXTNAME</span><span class="o">=</span>ext77

<span class="nv">ACTIONIT</span><span class="o">=</span>1

<span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$ACTIONIT</span><span class="s2">"</span> <span class="nt">-gt</span> <span class="s2">"0"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"we going to action it"</span>
  <span class="c"># backup current file</span>
  <span class="nb">export </span><span class="nv">BKUPNAME</span><span class="o">=</span><span class="s2">"config.</span><span class="sb">`</span><span class="nb">date</span> +%s<span class="sb">`</span><span class="s2">"</span>
  <span class="nb">cp</span> ~/.kube/config ~/.kube/<span class="nv">$BKUPNAME</span>
  <span class="nb">echo</span> <span class="s2">"current config backed up to ~/.kube/</span><span class="nv">$BKUPNAME</span><span class="s2">"</span>
<span class="k">else
  </span><span class="nb">echo</span> <span class="s2">"check only"</span>
<span class="k">fi</span>

<span class="c">#####</span>
<span class="c"># Secondary Int</span>

<span class="nb">echo</span> <span class="s2">"==========================="</span>
<span class="nb">echo</span> <span class="s2">" Checking </span><span class="nv">$SECONDARYINTNAME</span><span class="s2"> "</span>
<span class="nb">echo</span> <span class="s2">"==========================="</span>

<span class="nv">OLDCERTAUTHDATA</span><span class="o">=</span><span class="sb">`</span><span class="nb">cat</span> ~/.kube/config | yq <span class="nt">-r</span> <span class="s2">".clusters[] | select (.name == </span><span class="se">\"</span><span class="nv">$SECONDARYINTNAME</span><span class="se">\"</span><span class="s2">) | .cluster.</span><span class="se">\"</span><span class="s2">certificate-authority-data</span><span class="se">\"</span><span class="s2">"</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>
<span class="nv">NEWCERTAUTHDATA</span><span class="o">=</span><span class="sb">`</span>az keyvault secret show <span class="nt">--vault-name</span> idjakv <span class="nt">--name</span> int<span class="nv">$SECONDARYCPIP</span><span class="nt">-int</span> <span class="nt">--subscription</span> Pay-As-You-Go | jq <span class="nt">-r</span> .value | yq  <span class="nt">-r</span> <span class="s1">'.clusters[0].cluster."certificate-authority-data"'</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>

<span class="k">if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$OLDCERTAUTHDATA</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"</span><span class="nv">$NEWCERTAUTHDATA</span><span class="s2">"</span> <span class="o">]]</span>
<span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"cert auth data match"</span>
<span class="k">else
  if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$ACTIONIT</span><span class="s2">"</span> <span class="nt">-gt</span> <span class="s2">"0"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">echo</span> <span class="s2">"cert auth data different. updating"</span>
    <span class="nb">sed</span> <span class="nt">-i</span> <span class="s2">"s/</span><span class="nv">$OLDCERTAUTHDATA</span><span class="s2">/</span><span class="nv">$NEWCERTAUTHDATA</span><span class="s2">/g"</span> ~/.kube/config
  <span class="k">else
    </span><span class="nb">echo</span> <span class="s2">"cert auth data different"</span>
  <span class="k">fi
fi</span>

<span class="c"># Users</span>

<span class="nv">OLDCLIENTCERTDATA</span><span class="o">=</span><span class="sb">`</span><span class="nb">cat</span> ~/.kube/config | yq <span class="nt">-r</span> <span class="s2">".users[] | select (.name == </span><span class="se">\"</span><span class="nv">$SECONDARYINTNAME</span><span class="se">\"</span><span class="s2">) | .user.</span><span class="se">\"</span><span class="s2">client-certificate-data</span><span class="se">\"</span><span class="s2">"</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>
<span class="nv">NEWCLIENTCERTDATA</span><span class="o">=</span><span class="sb">`</span>az keyvault secret show <span class="nt">--vault-name</span> idjakv <span class="nt">--name</span> int<span class="nv">$SECONDARYCPIP</span><span class="nt">-int</span> <span class="nt">--subscription</span> Pay-As-You-Go | jq <span class="nt">-r</span> .value | yq <span class="nt">-r</span> <span class="s1">'.users[0].user."client-certificate-data"'</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>

<span class="nv">OLDCLIENTKEYDATA</span><span class="o">=</span><span class="sb">`</span><span class="nb">cat</span> ~/.kube/config | yq <span class="nt">-r</span> <span class="s2">".users[] | select (.name == </span><span class="se">\"</span><span class="nv">$SECONDARYINTNAME</span><span class="se">\"</span><span class="s2">) | .user.</span><span class="se">\"</span><span class="s2">client-key-data</span><span class="se">\"</span><span class="s2">"</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>
<span class="nv">NEWCLIENTKEYDATA</span><span class="o">=</span><span class="sb">`</span>az keyvault secret show <span class="nt">--vault-name</span> idjakv <span class="nt">--name</span> int<span class="nv">$SECONDARYCPIP</span><span class="nt">-int</span> <span class="nt">--subscription</span> Pay-As-You-Go | jq <span class="nt">-r</span> .value | yq <span class="nt">-r</span> <span class="s1">'.users[0].user."client-key-data"'</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>

<span class="k">if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$OLDCLIENTCERTDATA</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">""</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"No client cert data found for </span><span class="nv">$SECONDARYINTNAME</span><span class="s2">. skipping"</span>
<span class="k">else
  if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$OLDCLIENTCERTDATA</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"</span><span class="nv">$NEWCLIENTCERTDATA</span><span class="s2">"</span> <span class="o">]]</span>
  <span class="k">then
    </span><span class="nb">echo</span> <span class="s2">"client cert data match"</span>
  <span class="k">else
    if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$ACTIONIT</span><span class="s2">"</span> <span class="nt">-gt</span> <span class="s2">"0"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
      </span><span class="nb">echo</span> <span class="s2">"client cert data different. updating"</span>
      <span class="nb">sed</span> <span class="nt">-i</span> <span class="s2">"s/</span><span class="nv">$OLDCLIENTCERTDATA</span><span class="s2">/</span><span class="nv">$NEWCLIENTCERTDATA</span><span class="s2">/g"</span> ~/.kube/config
    <span class="k">else
      </span><span class="nb">echo</span> <span class="s2">"client cert data different."</span>
    <span class="k">fi
  fi
fi

if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$OLDCLIENTKEYDATA</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">""</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"No client key data found for </span><span class="nv">$SECONDARYINTNAME</span><span class="s2">. skipping"</span>
<span class="k">else
  if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$OLDCLIENTKEYDATA</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"</span><span class="nv">$NEWCLIENTKEYDATA</span><span class="s2">"</span> <span class="o">]]</span>
  <span class="k">then
    </span><span class="nb">echo</span> <span class="s2">"client key data match"</span>
  <span class="k">else
    if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$ACTIONIT</span><span class="s2">"</span> <span class="nt">-gt</span> <span class="s2">"0"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
      </span><span class="nb">echo</span> <span class="s2">"client key data different. updating"</span>
      <span class="nb">sed</span> <span class="nt">-i</span> <span class="s2">"s/</span><span class="nv">$OLDCLIENTKEYDATA</span><span class="s2">/</span><span class="nv">$NEWCLIENTKEYDATA</span><span class="s2">/g"</span> ~/.kube/config
    <span class="k">else
      </span><span class="nb">echo</span> <span class="s2">"client key data different."</span>
    <span class="k">fi
  fi
fi</span>

<span class="c">#####</span>
<span class="c"># Secondary Int</span>

<span class="nb">echo</span> <span class="s2">"==========================="</span>
<span class="nb">echo</span> <span class="s2">" Checking </span><span class="nv">$SECONDARYEXTNAME</span><span class="s2"> "</span>
<span class="nb">echo</span> <span class="s2">"==========================="</span>

<span class="nv">OLDCERTAUTHDATA</span><span class="o">=</span><span class="sb">`</span><span class="nb">cat</span> ~/.kube/config | yq <span class="nt">-r</span> <span class="s2">".clusters[] | select (.name == </span><span class="se">\"</span><span class="nv">$SECONDARYEXTNAME</span><span class="se">\"</span><span class="s2">) | .cluster.</span><span class="se">\"</span><span class="s2">certificate-authority-data</span><span class="se">\"</span><span class="s2">"</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>
<span class="nv">NEWCERTAUTHDATA</span><span class="o">=</span><span class="sb">`</span>az keyvault secret show <span class="nt">--vault-name</span> idjakv <span class="nt">--name</span> int<span class="nv">$SECONDARYCPIP</span><span class="nt">-int</span> <span class="nt">--subscription</span> Pay-As-You-Go | jq <span class="nt">-r</span> .value | yq  <span class="nt">-r</span> <span class="s1">'.clusters[0].cluster."certificate-authority-data"'</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>

<span class="k">if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$OLDCERTAUTHDATA</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"</span><span class="nv">$NEWCERTAUTHDATA</span><span class="s2">"</span> <span class="o">]]</span>
<span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"cert auth data match"</span>
<span class="k">else
  if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$ACTIONIT</span><span class="s2">"</span> <span class="nt">-gt</span> <span class="s2">"0"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">echo</span> <span class="s2">"cert auth data different. updating"</span>
    <span class="nb">sed</span> <span class="nt">-i</span> <span class="s2">"s/</span><span class="nv">$OLDCERTAUTHDATA</span><span class="s2">/</span><span class="nv">$NEWCERTAUTHDATA</span><span class="s2">/g"</span> ~/.kube/config
  <span class="k">else
    </span><span class="nb">echo</span> <span class="s2">"cert auth data different"</span>
  <span class="k">fi
fi</span>

<span class="c"># Users</span>

<span class="nv">OLDCLIENTCERTDATA</span><span class="o">=</span><span class="sb">`</span><span class="nb">cat</span> ~/.kube/config | yq <span class="nt">-r</span> <span class="s2">".users[] | select (.name == </span><span class="se">\"</span><span class="nv">$SECONDARYEXTNAME</span><span class="se">\"</span><span class="s2">) | .user.</span><span class="se">\"</span><span class="s2">client-certificate-data</span><span class="se">\"</span><span class="s2">"</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>
<span class="nv">NEWCLIENTCERTDATA</span><span class="o">=</span><span class="sb">`</span>az keyvault secret show <span class="nt">--vault-name</span> idjakv <span class="nt">--name</span> int<span class="nv">$SECONDARYCPIP</span><span class="nt">-int</span> <span class="nt">--subscription</span> Pay-As-You-Go | jq <span class="nt">-r</span> .value | yq <span class="nt">-r</span> <span class="s1">'.users[0].user."client-certificate-data"'</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>

<span class="nv">OLDCLIENTKEYDATA</span><span class="o">=</span><span class="sb">`</span><span class="nb">cat</span> ~/.kube/config | yq <span class="nt">-r</span> <span class="s2">".users[] | select (.name == </span><span class="se">\"</span><span class="nv">$SECONDARYEXTNAME</span><span class="se">\"</span><span class="s2">) | .user.</span><span class="se">\"</span><span class="s2">client-key-data</span><span class="se">\"</span><span class="s2">"</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>
<span class="nv">NEWCLIENTKEYDATA</span><span class="o">=</span><span class="sb">`</span>az keyvault secret show <span class="nt">--vault-name</span> idjakv <span class="nt">--name</span> int<span class="nv">$SECONDARYCPIP</span><span class="nt">-int</span> <span class="nt">--subscription</span> Pay-As-You-Go | jq <span class="nt">-r</span> .value | yq <span class="nt">-r</span> <span class="s1">'.users[0].user."client-key-data"'</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>

<span class="k">if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$OLDCLIENTCERTDATA</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">""</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"No client cert data found for </span><span class="nv">$SECONDARYEXTNAME</span><span class="s2">. skipping"</span>
<span class="k">else
  if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$OLDCLIENTCERTDATA</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"</span><span class="nv">$NEWCLIENTCERTDATA</span><span class="s2">"</span> <span class="o">]]</span>
  <span class="k">then
    </span><span class="nb">echo</span> <span class="s2">"client cert data match"</span>
  <span class="k">else
    if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$ACTIONIT</span><span class="s2">"</span> <span class="nt">-gt</span> <span class="s2">"0"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
      </span><span class="nb">echo</span> <span class="s2">"client cert data different. updating"</span>
      <span class="nb">sed</span> <span class="nt">-i</span> <span class="s2">"s/</span><span class="nv">$OLDCLIENTCERTDATA</span><span class="s2">/</span><span class="nv">$NEWCLIENTCERTDATA</span><span class="s2">/g"</span> ~/.kube/config
    <span class="k">else
      </span><span class="nb">echo</span> <span class="s2">"client cert data different."</span>
    <span class="k">fi
  fi
fi

if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$OLDCLIENTKEYDATA</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">""</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"No client key data found for </span><span class="nv">$SECONDARYEXTNAME</span><span class="s2">. skipping"</span>
<span class="k">else
  if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$OLDCLIENTKEYDATA</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"</span><span class="nv">$NEWCLIENTKEYDATA</span><span class="s2">"</span> <span class="o">]]</span>
  <span class="k">then
    </span><span class="nb">echo</span> <span class="s2">"client key data match"</span>
  <span class="k">else
    if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$ACTIONIT</span><span class="s2">"</span> <span class="nt">-gt</span> <span class="s2">"0"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
      </span><span class="nb">echo</span> <span class="s2">"client key data different. updating"</span>
      <span class="nb">sed</span> <span class="nt">-i</span> <span class="s2">"s/</span><span class="nv">$OLDCLIENTKEYDATA</span><span class="s2">/</span><span class="nv">$NEWCLIENTKEYDATA</span><span class="s2">/g"</span> ~/.kube/config
    <span class="k">else
      </span><span class="nb">echo</span> <span class="s2">"client key data different."</span>
    <span class="k">fi
  fi
fi</span>
</code></pre></div></div>

<p>This works just fine to update</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ./test.sh
we going to action it
current config backed up to ~/.kube/config.1774349638
===========================
 Checking mac77
===========================
cert auth data match
client cert data match
client key data match
===========================
 Checking ext77
===========================
cert auth data match
No client cert data found for ext77. skipping
No client key data found for ext77. skipping
</code></pre></div></div>

<p>I realized there was one more piece I neglected - the external IP, which does change from time to time</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectx ext77
Switched to context "ext77".
$ kubectl get nodes
E0324 05:56:49.548034   96004 memcache.go:265] "Unhandled Error" err="couldn't get current server API group list: Get \"https://75.72.233.202:22222/api?timeout=32s\": dial tcp 75.72.233.202:22222: i/o timeout"
</code></pre></div></div>

<p>I’ll add a block to test that, knowing that I always keep my <code class="language-plaintext highlighter-rouge">harbor.freshbrewed.science</code> updated (as without it, my blogging system falls down)</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">cat </span>test.sh
<span class="c">#!/bin/bash</span>

<span class="c"># Current Mapping</span>
<span class="nv">SECONDARYCPIP</span><span class="o">=</span>77
<span class="nv">SECONDARYINTNAME</span><span class="o">=</span>mac77
<span class="nv">SECONDARYEXTNAME</span><span class="o">=</span>ext77

<span class="nv">ACTIONIT</span><span class="o">=</span>1

<span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$ACTIONIT</span><span class="s2">"</span> <span class="nt">-gt</span> <span class="s2">"0"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"we going to action it"</span>
  <span class="c"># backup current file</span>
  <span class="nb">export </span><span class="nv">BKUPNAME</span><span class="o">=</span><span class="s2">"config.</span><span class="sb">`</span><span class="nb">date</span> +%s<span class="sb">`</span><span class="s2">"</span>
  <span class="nb">cp</span> ~/.kube/config ~/.kube/<span class="nv">$BKUPNAME</span>
  <span class="nb">echo</span> <span class="s2">"current config backed up to ~/.kube/</span><span class="nv">$BKUPNAME</span><span class="s2">"</span>
<span class="k">else
  </span><span class="nb">echo</span> <span class="s2">"check only"</span>
<span class="k">fi</span>

<span class="c">#####</span>
<span class="c"># Secondary Int</span>

<span class="nb">echo</span> <span class="s2">"==========================="</span>
<span class="nb">echo</span> <span class="s2">" Checking </span><span class="nv">$SECONDARYINTNAME</span><span class="s2"> "</span>
<span class="nb">echo</span> <span class="s2">"==========================="</span>

<span class="nv">OLDCERTAUTHDATA</span><span class="o">=</span><span class="sb">`</span><span class="nb">cat</span> ~/.kube/config | yq <span class="nt">-r</span> <span class="s2">".clusters[] | select (.name == </span><span class="se">\"</span><span class="nv">$SECONDARYINTNAME</span><span class="se">\"</span><span class="s2">) | .cluster.</span><span class="se">\"</span><span class="s2">certificate-authority-data</span><span class="se">\"</span><span class="s2">"</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>
<span class="nv">NEWCERTAUTHDATA</span><span class="o">=</span><span class="sb">`</span>az keyvault secret show <span class="nt">--vault-name</span> idjakv <span class="nt">--name</span> int<span class="nv">$SECONDARYCPIP</span><span class="nt">-int</span> <span class="nt">--subscription</span> Pay-As-You-Go | jq <span class="nt">-r</span> .value | yq  <span class="nt">-r</span> <span class="s1">'.clusters[0].cluster."certificate-authority-data"'</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>

<span class="k">if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$OLDCERTAUTHDATA</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"</span><span class="nv">$NEWCERTAUTHDATA</span><span class="s2">"</span> <span class="o">]]</span>
<span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"cert auth data match"</span>
<span class="k">else
  if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$ACTIONIT</span><span class="s2">"</span> <span class="nt">-gt</span> <span class="s2">"0"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">echo</span> <span class="s2">"cert auth data different. updating"</span>
    <span class="nb">sed</span> <span class="nt">-i</span> <span class="s2">"s/</span><span class="nv">$OLDCERTAUTHDATA</span><span class="s2">/</span><span class="nv">$NEWCERTAUTHDATA</span><span class="s2">/g"</span> ~/.kube/config
  <span class="k">else
    </span><span class="nb">echo</span> <span class="s2">"cert auth data different"</span>
  <span class="k">fi
fi</span>

<span class="c"># Users</span>

<span class="nv">OLDCLIENTCERTDATA</span><span class="o">=</span><span class="sb">`</span><span class="nb">cat</span> ~/.kube/config | yq <span class="nt">-r</span> <span class="s2">".users[] | select (.name == </span><span class="se">\"</span><span class="nv">$SECONDARYINTNAME</span><span class="se">\"</span><span class="s2">) | .user.</span><span class="se">\"</span><span class="s2">client-certificate-data</span><span class="se">\"</span><span class="s2">"</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>
<span class="nv">NEWCLIENTCERTDATA</span><span class="o">=</span><span class="sb">`</span>az keyvault secret show <span class="nt">--vault-name</span> idjakv <span class="nt">--name</span> int<span class="nv">$SECONDARYCPIP</span><span class="nt">-int</span> <span class="nt">--subscription</span> Pay-As-You-Go | jq <span class="nt">-r</span> .value | yq <span class="nt">-r</span> <span class="s1">'.users[0].user."client-certificate-data"'</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>

<span class="nv">OLDCLIENTKEYDATA</span><span class="o">=</span><span class="sb">`</span><span class="nb">cat</span> ~/.kube/config | yq <span class="nt">-r</span> <span class="s2">".users[] | select (.name == </span><span class="se">\"</span><span class="nv">$SECONDARYINTNAME</span><span class="se">\"</span><span class="s2">) | .user.</span><span class="se">\"</span><span class="s2">client-key-data</span><span class="se">\"</span><span class="s2">"</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>
<span class="nv">NEWCLIENTKEYDATA</span><span class="o">=</span><span class="sb">`</span>az keyvault secret show <span class="nt">--vault-name</span> idjakv <span class="nt">--name</span> int<span class="nv">$SECONDARYCPIP</span><span class="nt">-int</span> <span class="nt">--subscription</span> Pay-As-You-Go | jq <span class="nt">-r</span> .value | yq <span class="nt">-r</span> <span class="s1">'.users[0].user."client-key-data"'</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>

<span class="k">if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$OLDCLIENTCERTDATA</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">""</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"No client cert data found for </span><span class="nv">$SECONDARYINTNAME</span><span class="s2">. skipping"</span>
<span class="k">else
  if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$OLDCLIENTCERTDATA</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"</span><span class="nv">$NEWCLIENTCERTDATA</span><span class="s2">"</span> <span class="o">]]</span>
  <span class="k">then
    </span><span class="nb">echo</span> <span class="s2">"client cert data match"</span>
  <span class="k">else
    if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$ACTIONIT</span><span class="s2">"</span> <span class="nt">-gt</span> <span class="s2">"0"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
      </span><span class="nb">echo</span> <span class="s2">"client cert data different. updating"</span>
      <span class="nb">sed</span> <span class="nt">-i</span> <span class="s2">"s/</span><span class="nv">$OLDCLIENTCERTDATA</span><span class="s2">/</span><span class="nv">$NEWCLIENTCERTDATA</span><span class="s2">/g"</span> ~/.kube/config
    <span class="k">else
      </span><span class="nb">echo</span> <span class="s2">"client cert data different."</span>
    <span class="k">fi
  fi
fi

if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$OLDCLIENTKEYDATA</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">""</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"No client key data found for </span><span class="nv">$SECONDARYINTNAME</span><span class="s2">. skipping"</span>
<span class="k">else
  if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$OLDCLIENTKEYDATA</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"</span><span class="nv">$NEWCLIENTKEYDATA</span><span class="s2">"</span> <span class="o">]]</span>
  <span class="k">then
    </span><span class="nb">echo</span> <span class="s2">"client key data match"</span>
  <span class="k">else
    if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$ACTIONIT</span><span class="s2">"</span> <span class="nt">-gt</span> <span class="s2">"0"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
      </span><span class="nb">echo</span> <span class="s2">"client key data different. updating"</span>
      <span class="nb">sed</span> <span class="nt">-i</span> <span class="s2">"s/</span><span class="nv">$OLDCLIENTKEYDATA</span><span class="s2">/</span><span class="nv">$NEWCLIENTKEYDATA</span><span class="s2">/g"</span> ~/.kube/config
    <span class="k">else
      </span><span class="nb">echo</span> <span class="s2">"client key data different."</span>
    <span class="k">fi
  fi
fi</span>

<span class="c">#####</span>
<span class="c"># Secondary Ext</span>

<span class="nb">echo</span> <span class="s2">"==========================="</span>
<span class="nb">echo</span> <span class="s2">" Checking </span><span class="nv">$SECONDARYEXTNAME</span><span class="s2"> "</span>
<span class="nb">echo</span> <span class="s2">"==========================="</span>

<span class="nv">MYEXTIP</span><span class="o">=</span><span class="sb">`</span>dig +short A harbor.freshbrewed.science | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>
<span class="nv">OLDEXTIP</span><span class="o">=</span><span class="sb">`</span><span class="nb">cat</span> ~/.kube/config | yq <span class="nt">-r</span> <span class="s2">".clusters[] | select (.name == </span><span class="se">\"</span><span class="nv">$SECONDARYEXTNAME</span><span class="se">\"</span><span class="s2">) | .cluster.server"</span> | <span class="nb">sed</span> <span class="s1">'s/^.*:\/\///'</span> | <span class="nb">sed</span> <span class="s1">'s/:.*//'</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>

<span class="k">if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$OLDEXTIP</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"</span><span class="nv">$MYEXTIP</span><span class="s2">"</span> <span class="o">]]</span>
<span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"ext IPs are a match"</span>
<span class="k">else
  if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$ACTIONIT</span><span class="s2">"</span> <span class="nt">-gt</span> <span class="s2">"0"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">echo</span> <span class="s2">"ext IPs are different: Old </span><span class="nv">$OLDEXTIP</span><span class="s2"> and new </span><span class="nv">$MYEXTIP</span><span class="s2">. updating"</span>
    <span class="nb">sed</span> <span class="nt">-i</span> <span class="s2">"s/</span><span class="nv">$OLDEXTIP</span><span class="s2">/</span><span class="nv">$MYEXTIP</span><span class="s2">/g"</span> ~/.kube/config
  <span class="k">else
    </span><span class="nb">echo</span> <span class="s2">"ext IPs are different: Old </span><span class="nv">$OLDEXTIP</span><span class="s2"> and new </span><span class="nv">$MYEXTIP</span><span class="s2">"</span>
  <span class="k">fi
fi

</span><span class="nv">OLDCERTAUTHDATA</span><span class="o">=</span><span class="sb">`</span><span class="nb">cat</span> ~/.kube/config | yq <span class="nt">-r</span> <span class="s2">".clusters[] | select (.name == </span><span class="se">\"</span><span class="nv">$SECONDARYEXTNAME</span><span class="se">\"</span><span class="s2">) | .cluster.</span><span class="se">\"</span><span class="s2">certificate-authority-data</span><span class="se">\"</span><span class="s2">"</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>
<span class="nv">NEWCERTAUTHDATA</span><span class="o">=</span><span class="sb">`</span>az keyvault secret show <span class="nt">--vault-name</span> idjakv <span class="nt">--name</span> int<span class="nv">$SECONDARYCPIP</span><span class="nt">-int</span> <span class="nt">--subscription</span> Pay-As-You-Go | jq <span class="nt">-r</span> .value | yq  <span class="nt">-r</span> <span class="s1">'.clusters[0].cluster."certificate-authority-data"'</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>

<span class="k">if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$OLDCERTAUTHDATA</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"</span><span class="nv">$NEWCERTAUTHDATA</span><span class="s2">"</span> <span class="o">]]</span>
<span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"cert auth data match"</span>
<span class="k">else
  if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$ACTIONIT</span><span class="s2">"</span> <span class="nt">-gt</span> <span class="s2">"0"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">echo</span> <span class="s2">"cert auth data different. updating"</span>
    <span class="nb">sed</span> <span class="nt">-i</span> <span class="s2">"s/</span><span class="nv">$OLDCERTAUTHDATA</span><span class="s2">/</span><span class="nv">$NEWCERTAUTHDATA</span><span class="s2">/g"</span> ~/.kube/config
  <span class="k">else
    </span><span class="nb">echo</span> <span class="s2">"cert auth data different"</span>
  <span class="k">fi
fi</span>

<span class="c"># Users</span>

<span class="nv">OLDCLIENTCERTDATA</span><span class="o">=</span><span class="sb">`</span><span class="nb">cat</span> ~/.kube/config | yq <span class="nt">-r</span> <span class="s2">".users[] | select (.name == </span><span class="se">\"</span><span class="nv">$SECONDARYEXTNAME</span><span class="se">\"</span><span class="s2">) | .user.</span><span class="se">\"</span><span class="s2">client-certificate-data</span><span class="se">\"</span><span class="s2">"</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>
<span class="nv">NEWCLIENTCERTDATA</span><span class="o">=</span><span class="sb">`</span>az keyvault secret show <span class="nt">--vault-name</span> idjakv <span class="nt">--name</span> int<span class="nv">$SECONDARYCPIP</span><span class="nt">-int</span> <span class="nt">--subscription</span> Pay-As-You-Go | jq <span class="nt">-r</span> .value | yq <span class="nt">-r</span> <span class="s1">'.users[0].user."client-certificate-data"'</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>

<span class="nv">OLDCLIENTKEYDATA</span><span class="o">=</span><span class="sb">`</span><span class="nb">cat</span> ~/.kube/config | yq <span class="nt">-r</span> <span class="s2">".users[] | select (.name == </span><span class="se">\"</span><span class="nv">$SECONDARYEXTNAME</span><span class="se">\"</span><span class="s2">) | .user.</span><span class="se">\"</span><span class="s2">client-key-data</span><span class="se">\"</span><span class="s2">"</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>
<span class="nv">NEWCLIENTKEYDATA</span><span class="o">=</span><span class="sb">`</span>az keyvault secret show <span class="nt">--vault-name</span> idjakv <span class="nt">--name</span> int<span class="nv">$SECONDARYCPIP</span><span class="nt">-int</span> <span class="nt">--subscription</span> Pay-As-You-Go | jq <span class="nt">-r</span> .value | yq <span class="nt">-r</span> <span class="s1">'.users[0].user."client-key-data"'</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="sb">`</span>

<span class="k">if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$OLDCLIENTCERTDATA</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">""</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"No client cert data found for </span><span class="nv">$SECONDARYEXTNAME</span><span class="s2">. skipping"</span>
<span class="k">else
  if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$OLDCLIENTCERTDATA</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"</span><span class="nv">$NEWCLIENTCERTDATA</span><span class="s2">"</span> <span class="o">]]</span>
  <span class="k">then
    </span><span class="nb">echo</span> <span class="s2">"client cert data match"</span>
  <span class="k">else
    if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$ACTIONIT</span><span class="s2">"</span> <span class="nt">-gt</span> <span class="s2">"0"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
      </span><span class="nb">echo</span> <span class="s2">"client cert data different. updating"</span>
      <span class="nb">sed</span> <span class="nt">-i</span> <span class="s2">"s/</span><span class="nv">$OLDCLIENTCERTDATA</span><span class="s2">/</span><span class="nv">$NEWCLIENTCERTDATA</span><span class="s2">/g"</span> ~/.kube/config
    <span class="k">else
      </span><span class="nb">echo</span> <span class="s2">"client cert data different."</span>
    <span class="k">fi
  fi
fi

if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$OLDCLIENTKEYDATA</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">""</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"No client key data found for </span><span class="nv">$SECONDARYEXTNAME</span><span class="s2">. skipping"</span>
<span class="k">else
  if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$OLDCLIENTKEYDATA</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"</span><span class="nv">$NEWCLIENTKEYDATA</span><span class="s2">"</span> <span class="o">]]</span>
  <span class="k">then
    </span><span class="nb">echo</span> <span class="s2">"client key data match"</span>
  <span class="k">else
    if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$ACTIONIT</span><span class="s2">"</span> <span class="nt">-gt</span> <span class="s2">"0"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
      </span><span class="nb">echo</span> <span class="s2">"client key data different. updating"</span>
      <span class="nb">sed</span> <span class="nt">-i</span> <span class="s2">"s/</span><span class="nv">$OLDCLIENTKEYDATA</span><span class="s2">/</span><span class="nv">$NEWCLIENTKEYDATA</span><span class="s2">/g"</span> ~/.kube/config
    <span class="k">else
      </span><span class="nb">echo</span> <span class="s2">"client key data different."</span>
    <span class="k">fi
  fi
fi</span>
</code></pre></div></div>

<p>Let’s test</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ./test.sh
we going to action it
current config backed up to ~/.kube/config.1774350306
===========================
 Checking mac77
===========================
cert auth data match
client cert data match
client key data match
===========================
 Checking ext77
===========================
ext IPs are different: Old 75.72.233.202 and new 76.156.69.232. updating
cert auth data match
No client cert data found for ext77. skipping
No client key data found for ext77. skipping
$ kubectx ext77
Switched to context "ext77".
$ kubectl get nodes
NAME                    STATUS   ROLES           AGE     VERSION
builder-macbookpro8-1   Ready    &lt;none&gt;          5d21h   v1.35.1+k3s1
builder-macbookpro8-2   Ready    &lt;none&gt;          2d15h   v1.35.1+k3s1
isaac-macbookair        Ready    control-plane   6d10h   v1.35.1+k3s1
</code></pre></div></div>

<p>My next issue is I need to repeat all this for <code class="language-plaintext highlighter-rouge">int247-int</code> which is really <code class="language-plaintext highlighter-rouge">int33</code> and <code class="language-plaintext highlighter-rouge">ext33</code> in my kubeconfig, again, for historical reasons.</p>

<p>Now, this next block, which is just too damn long, does work</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cat ./test.sh
#!/bin/bash

# Current Mapping
SECONDARYCPIP=77
SECONDARYINTNAME=mac77
SECONDARYEXTNAME=ext77

ACTIONIT=1

if [ "$ACTIONIT" -gt "0" ]; then
  echo "we going to action it"
  # backup current file
  export BKUPNAME="config.`date +%s`"
  cp ~/.kube/config ~/.kube/$BKUPNAME
  echo "current config backed up to ~/.kube/$BKUPNAME"
else
  echo "check only"
fi

#####
# Secondary Int

echo "==========================="
echo " Checking $SECONDARYINTNAME "
echo "==========================="

OLDCERTAUTHDATA=`cat ~/.kube/config | yq -r ".clusters[] | select (.name == \"$SECONDARYINTNAME\") | .cluster.\"certificate-authority-data\"" | tr -d '\n'`
NEWCERTAUTHDATA=`az keyvault secret show --vault-name idjakv --name int$SECONDARYCPIP-int --subscription Pay-As-You-Go | jq -r .value | yq  -r '.clusters[0].cluster."certificate-authority-data"' | tr -d '\n'`

if [[ "$OLDCERTAUTHDATA" == "$NEWCERTAUTHDATA" ]]
then
  echo "cert auth data match"
else
  if [ "$ACTIONIT" -gt "0" ]; then
    echo "cert auth data different. updating"
    sed -i "s/$OLDCERTAUTHDATA/$NEWCERTAUTHDATA/g" ~/.kube/config
  else
    echo "cert auth data different"
  fi
fi

# Users

OLDCLIENTCERTDATA=`cat ~/.kube/config | yq -r ".users[] | select (.name == \"$SECONDARYINTNAME\") | .user.\"client-certificate-data\"" | tr -d '\n'`
NEWCLIENTCERTDATA=`az keyvault secret show --vault-name idjakv --name int$SECONDARYCPIP-int --subscription Pay-As-You-Go | jq -r .value | yq -r '.users[0].user."client-certificate-data"' | tr -d '\n'`

OLDCLIENTKEYDATA=`cat ~/.kube/config | yq -r ".users[] | select (.name == \"$SECONDARYINTNAME\") | .user.\"client-key-data\"" | tr -d '\n'`
NEWCLIENTKEYDATA=`az keyvault secret show --vault-name idjakv --name int$SECONDARYCPIP-int --subscription Pay-As-You-Go | jq -r .value | yq -r '.users[0].user."client-key-data"' | tr -d '\n'`

if [[ "$OLDCLIENTCERTDATA" == "" ]]; then
  echo "No client cert data found for $SECONDARYINTNAME. skipping"
else
  if [[ "$OLDCLIENTCERTDATA" == "$NEWCLIENTCERTDATA" ]]
  then
    echo "client cert data match"
  else
    if [ "$ACTIONIT" -gt "0" ]; then
      echo "client cert data different. updating"
      sed -i "s/$OLDCLIENTCERTDATA/$NEWCLIENTCERTDATA/g" ~/.kube/config
    else
      echo "client cert data different."
    fi
  fi
fi

if [[ "$OLDCLIENTKEYDATA" == "" ]]; then
  echo "No client key data found for $SECONDARYINTNAME. skipping"
else
  if [[ "$OLDCLIENTKEYDATA" == "$NEWCLIENTKEYDATA" ]]
  then
    echo "client key data match"
  else
    if [ "$ACTIONIT" -gt "0" ]; then
      echo "client key data different. updating"
      sed -i "s/$OLDCLIENTKEYDATA/$NEWCLIENTKEYDATA/g" ~/.kube/config
    else
      echo "client key data different."
    fi
  fi
fi

#####
# Secondary Ext

echo "==========================="
echo " Checking $SECONDARYEXTNAME "
echo "==========================="

MYEXTIP=`dig +short A harbor.freshbrewed.science | tr -d '\n'`
OLDEXTIP=`cat ~/.kube/config | yq -r ".clusters[] | select (.name == \"$SECONDARYEXTNAME\") | .cluster.server" | sed 's/^.*:\/\///' | sed 's/:.*//' | tr -d '\n'`

if [[ "$OLDEXTIP" == "$MYEXTIP" ]]
then
  echo "ext IPs are a match"
else
  if [ "$ACTIONIT" -gt "0" ]; then
    echo "ext IPs are different: Old $OLDEXTIP and new $MYEXTIP. updating"
    sed -i "s/$OLDEXTIP/$MYEXTIP/g" ~/.kube/config
  else
    echo "ext IPs are different: Old $OLDEXTIP and new $MYEXTIP"
  fi
fi

OLDCERTAUTHDATA=`cat ~/.kube/config | yq -r ".clusters[] | select (.name == \"$SECONDARYEXTNAME\") | .cluster.\"certificate-authority-data\"" | tr -d '\n'`
NEWCERTAUTHDATA=`az keyvault secret show --vault-name idjakv --name int$SECONDARYCPIP-int --subscription Pay-As-You-Go | jq -r .value | yq  -r '.clusters[0].cluster."certificate-authority-data"' | tr -d '\n'`

if [[ "$OLDCERTAUTHDATA" == "$NEWCERTAUTHDATA" ]]
then
  echo "cert auth data match"
else
  if [ "$ACTIONIT" -gt "0" ]; then
    echo "cert auth data different. updating"
    sed -i "s/$OLDCERTAUTHDATA/$NEWCERTAUTHDATA/g" ~/.kube/config
  else
    echo "cert auth data different"
  fi
fi

# Users

OLDCLIENTCERTDATA=`cat ~/.kube/config | yq -r ".users[] | select (.name == \"$SECONDARYEXTNAME\") | .user.\"client-certificate-data\"" | tr -d '\n'`
NEWCLIENTCERTDATA=`az keyvault secret show --vault-name idjakv --name int$SECONDARYCPIP-int --subscription Pay-As-You-Go | jq -r .value | yq -r '.users[0].user."client-certificate-data"' | tr -d '\n'`

OLDCLIENTKEYDATA=`cat ~/.kube/config | yq -r ".users[] | select (.name == \"$SECONDARYEXTNAME\") | .user.\"client-key-data\"" | tr -d '\n'`
NEWCLIENTKEYDATA=`az keyvault secret show --vault-name idjakv --name int$SECONDARYCPIP-int --subscription Pay-As-You-Go | jq -r .value | yq -r '.users[0].user."client-key-data"' | tr -d '\n'`

if [[ "$OLDCLIENTCERTDATA" == "" ]]; then
  echo "No client cert data found for $SECONDARYEXTNAME. skipping"
else
  if [[ "$OLDCLIENTCERTDATA" == "$NEWCLIENTCERTDATA" ]]
  then
    echo "client cert data match"
  else
    if [ "$ACTIONIT" -gt "0" ]; then
      echo "client cert data different. updating"
      sed -i "s/$OLDCLIENTCERTDATA/$NEWCLIENTCERTDATA/g" ~/.kube/config
    else
      echo "client cert data different."
    fi
  fi
fi

if [[ "$OLDCLIENTKEYDATA" == "" ]]; then
  echo "No client key data found for $SECONDARYEXTNAME. skipping"
else
  if [[ "$OLDCLIENTKEYDATA" == "$NEWCLIENTKEYDATA" ]]
  then
    echo "client key data match"
  else
    if [ "$ACTIONIT" -gt "0" ]; then
      echo "client key data different. updating"
      sed -i "s/$OLDCLIENTKEYDATA/$NEWCLIENTKEYDATA/g" ~/.kube/config
    else
      echo "client key data different."
    fi
  fi
fi

# Current Mapping
PRIMARYCPIP=247
PRIMARYINTNAME=int33
PRIMARYEXTNAME=ext33

#####
# Primary Int

echo "==========================="
echo " Checking $PRIMARYINTNAME "
echo "==========================="

OLDCERTAUTHDATA=`cat ~/.kube/config | yq -r ".clusters[] | select (.name == \"$PRIMARYINTNAME\") | .cluster.\"certificate-authority-data\"" | tr -d '\n'`
NEWCERTAUTHDATA=`az keyvault secret show --vault-name idjakv --name int$PRIMARYCPIP-int --subscription Pay-As-You-Go | jq -r .value | yq  -r '.clusters[0].cluster."certificate-authority-data"' | tr -d '\n'`

if [[ "$OLDCERTAUTHDATA" == "$NEWCERTAUTHDATA" ]]
then
  echo "cert auth data match"
else
  if [ "$ACTIONIT" -gt "0" ]; then
    echo "cert auth data different. updating"
    sed -i "s/$OLDCERTAUTHDATA/$NEWCERTAUTHDATA/g" ~/.kube/config
  else
    echo "cert auth data different"
  fi
fi

# Users

OLDCLIENTCERTDATA=`cat ~/.kube/config | yq -r ".users[] | select (.name == \"$PRIMARYINTNAME\") | .user.\"client-certificate-data\"" | tr -d '\n'`
NEWCLIENTCERTDATA=`az keyvault secret show --vault-name idjakv --name int$PRIMARYCPIP-int --subscription Pay-As-You-Go | jq -r .value | yq -r '.users[0].user."client-certificate-data"' | tr -d '\n'`

OLDCLIENTKEYDATA=`cat ~/.kube/config | yq -r ".users[] | select (.name == \"$PRIMARYINTNAME\") | .user.\"client-key-data\"" | tr -d '\n'`
NEWCLIENTKEYDATA=`az keyvault secret show --vault-name idjakv --name int$PRIMARYCPIP-int --subscription Pay-As-You-Go | jq -r .value | yq -r '.users[0].user."client-key-data"' | tr -d '\n'`

if [[ "$OLDCLIENTCERTDATA" == "" ]]; then
  echo "No client cert data found for $PRIMARYINTNAME. skipping"
else
  if [[ "$OLDCLIENTCERTDATA" == "$NEWCLIENTCERTDATA" ]]
  then
    echo "client cert data match"
  else
    if [ "$ACTIONIT" -gt "0" ]; then
      echo "client cert data different. updating"
      sed -i "s/$OLDCLIENTCERTDATA/$NEWCLIENTCERTDATA/g" ~/.kube/config
    else
      echo "client cert data different."
    fi
  fi
fi

if [[ "$OLDCLIENTKEYDATA" == "" ]]; then
  echo "No client key data found for $PRIMARYINTNAME. skipping"
else
  if [[ "$OLDCLIENTKEYDATA" == "$NEWCLIENTKEYDATA" ]]
  then
    echo "client key data match"
  else
    if [ "$ACTIONIT" -gt "0" ]; then
      echo "client key data different. updating"
      sed -i "s/$OLDCLIENTKEYDATA/$NEWCLIENTKEYDATA/g" ~/.kube/config
    else
      echo "client key data different."
    fi
  fi
fi

#####
# Primary Ext

echo "==========================="
echo " Checking $PRIMARYEXTNAME "
echo "==========================="

MYEXTIP=`dig +short A harbor.freshbrewed.science | tr -d '\n'`
OLDEXTIP=`cat ~/.kube/config | yq -r ".clusters[] | select (.name == \"$PRIMARYEXTNAME\") | .cluster.server" | sed 's/^.*:\/\///' | sed 's/:.*//' | tr -d '\n'`

if [[ "$OLDEXTIP" == "$MYEXTIP" ]]
then
  echo "ext IPs are a match"
else
  if [ "$ACTIONIT" -gt "0" ]; then
    echo "ext IPs are different: Old $OLDEXTIP and new $MYEXTIP. updating"
    sed -i "s/$OLDEXTIP/$MYEXTIP/g" ~/.kube/config
  else
    echo "ext IPs are different: Old $OLDEXTIP and new $MYEXTIP"
  fi
fi

OLDCERTAUTHDATA=`cat ~/.kube/config | yq -r ".clusters[] | select (.name == \"$PRIMARYEXTNAME\") | .cluster.\"certificate-authority-data\"" | tr -d '\n'`
NEWCERTAUTHDATA=`az keyvault secret show --vault-name idjakv --name int$PRIMARYCPIP-int --subscription Pay-As-You-Go | jq -r .value | yq  -r '.clusters[0].cluster."certificate-authority-data"' | tr -d '\n'`

if [[ "$OLDCERTAUTHDATA" == "$NEWCERTAUTHDATA" ]]
then
  echo "cert auth data match"
else
  if [ "$ACTIONIT" -gt "0" ]; then
    echo "cert auth data different. updating"
    sed -i "s/$OLDCERTAUTHDATA/$NEWCERTAUTHDATA/g" ~/.kube/config
  else
    echo "cert auth data different"
  fi
fi

# Users

OLDCLIENTCERTDATA=`cat ~/.kube/config | yq -r ".users[] | select (.name == \"$PRIMARYEXTNAME\") | .user.\"client-certificate-data\"" | tr -d '\n'`
NEWCLIENTCERTDATA=`az keyvault secret show --vault-name idjakv --name int$PRIMARYCPIP-int --subscription Pay-As-You-Go | jq -r .value | yq -r '.users[0].user."client-certificate-data"' | tr -d '\n'`

OLDCLIENTKEYDATA=`cat ~/.kube/config | yq -r ".users[] | select (.name == \"$PRIMARYEXTNAME\") | .user.\"client-key-data\"" | tr -d '\n'`
NEWCLIENTKEYDATA=`az keyvault secret show --vault-name idjakv --name int$PRIMARYCPIP-int --subscription Pay-As-You-Go | jq -r .value | yq -r '.users[0].user."client-key-data"' | tr -d '\n'`

if [[ "$OLDCLIENTCERTDATA" == "" ]]; then
  echo "No client cert data found for $PRIMARYEXTNAME. skipping"
else
  if [[ "$OLDCLIENTCERTDATA" == "$NEWCLIENTCERTDATA" ]]
  then
    echo "client cert data match"
  else
    if [ "$ACTIONIT" -gt "0" ]; then
      echo "client cert data different. updating"
      sed -i "s/$OLDCLIENTCERTDATA/$NEWCLIENTCERTDATA/g" ~/.kube/config
    else
      echo "client cert data different."
    fi
  fi
fi

if [[ "$OLDCLIENTKEYDATA" == "" ]]; then
  echo "No client key data found for $PRIMARYEXTNAME. skipping"
else
  if [[ "$OLDCLIENTKEYDATA" == "$NEWCLIENTKEYDATA" ]]
  then
    echo "client key data match"
  else
    if [ "$ACTIONIT" -gt "0" ]; then
      echo "client key data different. updating"
      sed -i "s/$OLDCLIENTKEYDATA/$NEWCLIENTKEYDATA/g" ~/.kube/config
    else
      echo "client key data different."
    fi
  fi
fi
</code></pre></div></div>

<p>We can see it does the job</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ./test.sh
we going to action it
current config backed up to ~/.kube/config.1774350712
===========================
 Checking mac77
===========================
cert auth data match
client cert data match
client key data match
===========================
 Checking ext77
===========================
ext IPs are a match
cert auth data match
No client cert data found for ext77. skipping
No client key data found for ext77. skipping
===========================
 Checking int33
===========================
cert auth data match
client cert data match
client key data match
===========================
 Checking ext33
===========================
ext IPs are a match
cert auth data match
No client cert data found for ext33. skipping
No client key data found for ext33. skipping
</code></pre></div></div>

<p>But seriously, that is a gnarly long unoptimized script.</p>

<h2 id="a-little-touch-of-llm-help">A little touch of LLM help</h2>

<p>I copied over the ++verbose script to a new folder</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/jekyll-blog$ cd ..
builder@DESKTOP-QADGF36:~/Workspaces$ mkdir k8scheck
builder@DESKTOP-QADGF36:~/Workspaces$ cd k8scheck/
builder@DESKTOP-QADGF36:~/Workspaces/k8scheck$ cp ../jekyll-blog/test.sh
cp: missing destination file operand after '../jekyll-blog/test.sh'
Try 'cp --help' for more information.
builder@DESKTOP-QADGF36:~/Workspaces/k8scheck$ cp ../jekyll-blog/test.sh ./checkandfix.sh
builder@DESKTOP-QADGF36:~/Workspaces/k8scheck$ ls
checkandfix.sh
</code></pre></div></div>

<p>I asked Gemini CLI for help</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>My @checkandfix.sh works, however it is very unoptimized with lots of repeated blocks.  Please clean this script up with a focus on optimizing
   and reuse.  Add comments to any created functions or subroutines.
</code></pre></div></div>

<p>What is usually really quite fast took nearly 2 minutes</p>

<video muted="" controls="">
    <source src="/content/images/2026/03/ansible-04.mp4" type="video/mp4" />
</video>

<p>It also failed to update the file. So i had to grab the output from the console and do it myself.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">cat</span> ./checkandfix.sh
<span class="c">#!/bin/bash</span>

<span class="c"># Configuration</span>
<span class="nv">VAULT_NAME</span><span class="o">=</span><span class="s2">"idjakv"</span>
<span class="nv">SUBSCRIPTION</span><span class="o">=</span><span class="s2">"Pay-As-You-Go"</span>
<span class="nv">KUBECONFIG</span><span class="o">=</span><span class="s2">"</span><span class="nv">$HOME</span><span class="s2">/.kube/config"</span>
<span class="nv">ACTIONIT</span><span class="o">=</span>1

<span class="c"># --- Functions ---</span>

<span class="c"># Backs up the current kubeconfig if ACTIONIT is set</span>
backup_config<span class="o">()</span> <span class="o">{</span>
    <span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$ACTIONIT</span><span class="s2">"</span> <span class="nt">-gt</span> <span class="s2">"0"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
        </span><span class="nb">local </span><span class="nv">backup_name</span><span class="o">=</span><span class="s2">"config.</span><span class="si">$(</span><span class="nb">date</span> +%s<span class="si">)</span><span class="s2">"</span>
        <span class="nb">cp</span> <span class="s2">"</span><span class="nv">$KUBECONFIG</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$HOME</span><span class="s2">/.kube/</span><span class="nv">$backup_name</span><span class="s2">"</span>
        <span class="nb">echo</span> <span class="s2">"Current config backed up to ~/.kube/</span><span class="nv">$backup_name</span><span class="s2">"</span>
    <span class="k">else
        </span><span class="nb">echo</span> <span class="s2">"Check-only mode. No backup created."</span>
    <span class="k">fi</span>
<span class="o">}</span>

<span class="c"># Compares old and new data and updates the config using sed if ACTIONIT is set</span>
<span class="c"># Usage: check_and_update &lt;label&gt; &lt;old_value&gt; &lt;new_value&gt;</span>
check_and_update<span class="o">()</span> <span class="o">{</span>
    <span class="nb">local </span><span class="nv">label</span><span class="o">=</span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span>
    <span class="nb">local </span><span class="nv">old_val</span><span class="o">=</span><span class="s2">"</span><span class="nv">$2</span><span class="s2">"</span>
    <span class="nb">local </span><span class="nv">new_val</span><span class="o">=</span><span class="s2">"</span><span class="nv">$3</span><span class="s2">"</span>

    <span class="k">if</span> <span class="o">[[</span> <span class="nt">-z</span> <span class="s2">"</span><span class="nv">$old_val</span><span class="s2">"</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
        </span><span class="nb">echo</span> <span class="s2">"No </span><span class="nv">$label</span><span class="s2"> found. Skipping."</span>
        <span class="k">return
    fi

    if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$old_val</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"</span><span class="nv">$new_val</span><span class="s2">"</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
        </span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$label</span><span class="s2"> matches."</span>
    <span class="k">else
        if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$ACTIONIT</span><span class="s2">"</span> <span class="nt">-gt</span> <span class="s2">"0"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
            </span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$label</span><span class="s2"> different. Updating..."</span>
            <span class="c"># Using | as delimiter in case values contain /</span>
            <span class="nb">sed</span> <span class="nt">-i</span> <span class="s2">"s|</span><span class="nv">$old_val</span><span class="s2">|</span><span class="nv">$new_val</span><span class="s2">|g"</span> <span class="s2">"</span><span class="nv">$KUBECONFIG</span><span class="s2">"</span>
        <span class="k">else
            </span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$label</span><span class="s2"> different."</span>
        <span class="k">fi
    fi</span>
<span class="o">}</span>

<span class="c"># Processes a specific cluster context (Cluster and User entries)</span>
<span class="c"># Usage: process_context &lt;context_name&gt; &lt;secret_data_yaml&gt; &lt;is_external_flag&gt;</span>
process_context<span class="o">()</span> <span class="o">{</span>
    <span class="nb">local </span><span class="nv">name</span><span class="o">=</span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span>
    <span class="nb">local </span><span class="nv">secret_data</span><span class="o">=</span><span class="s2">"</span><span class="nv">$2</span><span class="s2">"</span>
    <span class="nb">local </span><span class="nv">is_ext</span><span class="o">=</span><span class="s2">"</span><span class="nv">$3</span><span class="s2">"</span>

    <span class="nb">echo</span> <span class="s2">"==========================="</span>
    <span class="nb">echo</span> <span class="s2">" Checking </span><span class="nv">$name</span><span class="s2"> "</span>
    <span class="nb">echo</span> <span class="s2">"==========================="</span>

    <span class="c"># 1. Handle IP check for external endpoints</span>
    <span class="k">if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$is_ext</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"true"</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
        </span><span class="nb">local </span><span class="nv">my_ext_ip</span><span class="o">=</span><span class="si">$(</span>dig +short A harbor.freshbrewed.science | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="si">)</span>
        <span class="nb">local </span><span class="nv">old_ext_ip</span><span class="o">=</span><span class="si">$(</span>yq <span class="nt">-r</span> <span class="s2">".clusters[] | select (.name == </span><span class="se">\"</span><span class="nv">$name</span><span class="se">\"</span><span class="s2">) | .cluster.server"</span> <span class="s2">"</span><span class="nv">$KUBECONFIG</span><span class="s2">"</span> | <span class="nb">sed</span> <span class="s1">'s|^.*://||;s|:.*||'</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="si">)</span>

        <span class="k">if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$old_ext_ip</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"</span><span class="nv">$my_ext_ip</span><span class="s2">"</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
            </span><span class="nb">echo</span> <span class="s2">"Ext IPs are a match."</span>
        <span class="k">else
            if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$ACTIONIT</span><span class="s2">"</span> <span class="nt">-gt</span> <span class="s2">"0"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
                </span><span class="nb">echo</span> <span class="s2">"Ext IPs differ: Old </span><span class="nv">$old_ext_ip</span><span class="s2">, New </span><span class="nv">$my_ext_ip</span><span class="s2">. Updating..."</span>
                <span class="nb">sed</span> <span class="nt">-i</span> <span class="s2">"s|</span><span class="nv">$old_ext_ip</span><span class="s2">|</span><span class="nv">$my_ext_ip</span><span class="s2">|g"</span> <span class="s2">"</span><span class="nv">$KUBECONFIG</span><span class="s2">"</span>
            <span class="k">else
                </span><span class="nb">echo</span> <span class="s2">"Ext IPs differ: Old </span><span class="nv">$old_ext_ip</span><span class="s2">, New </span><span class="nv">$my_ext_ip</span><span class="s2">."</span>
            <span class="k">fi
        fi
    fi</span>

    <span class="c"># 2. Extract current data from kubeconfig</span>
    <span class="nb">local </span><span class="nv">old_ca</span><span class="o">=</span><span class="si">$(</span>yq <span class="nt">-r</span> <span class="s2">".clusters[] | select (.name == </span><span class="se">\"</span><span class="nv">$name</span><span class="se">\"</span><span class="s2">) | .cluster.</span><span class="se">\"</span><span class="s2">certificate-authority-data</span><span class="se">\"</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$KUBECONFIG</span><span class="s2">"</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="si">)</span>
    <span class="nb">local </span><span class="nv">old_cert</span><span class="o">=</span><span class="si">$(</span>yq <span class="nt">-r</span> <span class="s2">".users[] | select (.name == </span><span class="se">\"</span><span class="nv">$name</span><span class="se">\"</span><span class="s2">) | .user.</span><span class="se">\"</span><span class="s2">client-certificate-data</span><span class="se">\"</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$KUBECONFIG</span><span class="s2">"</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="si">)</span>
    <span class="nb">local </span><span class="nv">old_key</span><span class="o">=</span><span class="si">$(</span>yq <span class="nt">-r</span> <span class="s2">".users[] | select (.name == </span><span class="se">\"</span><span class="nv">$name</span><span class="se">\"</span><span class="s2">) | .user.</span><span class="se">\"</span><span class="s2">client-key-data</span><span class="se">\"</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$KUBECONFIG</span><span class="s2">"</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="si">)</span>

    <span class="c"># 3. Extract new data from the secret (passed as a string)</span>
    <span class="nb">local </span><span class="nv">new_ca</span><span class="o">=</span><span class="si">$(</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$secret_data</span><span class="s2">"</span> | yq <span class="nt">-r</span> <span class="s1">'.clusters[0].cluster."certificate-authority-data"'</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="si">)</span>
    <span class="nb">local </span><span class="nv">new_cert</span><span class="o">=</span><span class="si">$(</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$secret_data</span><span class="s2">"</span> | yq <span class="nt">-r</span> <span class="s1">'.users[0].user."client-certificate-data"'</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="si">)</span>
    <span class="nb">local </span><span class="nv">new_key</span><span class="o">=</span><span class="si">$(</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$secret_data</span><span class="s2">"</span> | yq <span class="nt">-r</span> <span class="s1">'.users[0].user."client-key-data"'</span> | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">'\n'</span><span class="si">)</span>

    <span class="c"># 4. Perform comparisons and updates</span>
    check_and_update <span class="s2">"Cert auth data"</span> <span class="s2">"</span><span class="nv">$old_ca</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$new_ca</span><span class="s2">"</span>
    check_and_update <span class="s2">"Client cert data"</span> <span class="s2">"</span><span class="nv">$old_cert</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$new_cert</span><span class="s2">"</span>
    check_and_update <span class="s2">"Client key data"</span> <span class="s2">"</span><span class="nv">$old_key</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$new_key</span><span class="s2">"</span>
<span class="o">}</span>

<span class="c"># --- Main Logic ---</span>

backup_config

<span class="c"># Targets array format: "CPIP_SUFFIX:INT_NAME:EXT_NAME"</span>
<span class="nv">targets</span><span class="o">=(</span>
    <span class="s2">"77:mac77:ext77"</span>
    <span class="s2">"247:int33:ext33"</span>
<span class="o">)</span>

<span class="k">for </span>target <span class="k">in</span> <span class="s2">"</span><span class="k">${</span><span class="nv">targets</span><span class="p">[@]</span><span class="k">}</span><span class="s2">"</span><span class="p">;</span> <span class="k">do
    </span><span class="nv">IFS</span><span class="o">=</span><span class="s2">":"</span> <span class="nb">read</span> <span class="nt">-r</span> cpip int_name ext_name <span class="o">&lt;&lt;&lt;</span> <span class="s2">"</span><span class="nv">$target</span><span class="s2">"</span>

    <span class="nb">echo</span> <span class="s2">"Fetching secret for IP suffix </span><span class="nv">$cpip</span><span class="s2">..."</span>
    <span class="c"># Fetch the secret once per cluster set to optimize API calls</span>
    <span class="nv">secret_json</span><span class="o">=</span><span class="si">$(</span>az keyvault secret show <span class="nt">--vault-name</span> <span class="s2">"</span><span class="nv">$VAULT_NAME</span><span class="s2">"</span> <span class="nt">--name</span> <span class="s2">"int</span><span class="nv">$cpip</span><span class="s2">-int"</span> <span class="nt">--subscription</span> <span class="s2">"</span><span class="nv">$SUBSCRIPTION</span><span class="s2">"</span> | jq <span class="nt">-r</span> .value<span class="si">)</span>

    <span class="k">if</span> <span class="o">[[</span> <span class="nt">-z</span> <span class="s2">"</span><span class="nv">$secret_json</span><span class="s2">"</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
        </span><span class="nb">echo</span> <span class="s2">"Error: Could not retrieve secret for </span><span class="nv">$cpip</span><span class="s2">. Skipping."</span>
        <span class="k">continue
    fi

    </span>process_context <span class="s2">"</span><span class="nv">$int_name</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$secret_json</span><span class="s2">"</span> <span class="s2">"false"</span>
    process_context <span class="s2">"</span><span class="nv">$ext_name</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$secret_json</span><span class="s2">"</span> <span class="s2">"true"</span>
<span class="k">done</span>
</code></pre></div></div>

<p>This works</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ./checkandfix.sh
Current config backed up to ~/.kube/config.1774351669
Fetching secret for IP suffix 77...
===========================
 Checking mac77
===========================
Cert auth data matches.
Client cert data matches.
Client key data matches.
===========================
 Checking ext77
===========================
Ext IPs are a match.
Cert auth data matches.
No Client cert data found. Skipping.
No Client key data found. Skipping.
Fetching secret for IP suffix 247...
===========================
 Checking int33
===========================
Cert auth data matches.
Client cert data matches.
Client key data matches.
===========================
 Checking ext33
===========================
Ext IPs are a match.
Cert auth data matches.
No Client cert data found. Skipping.
No Client key data found. Skipping.

</code></pre></div></div>

<p>I’m just a little bit disappointed in Gemini CLI for being slow and not writing files.</p>

<h2 id="knowing-when-ip-shifts-happen">Knowing when IP shifts happen</h2>

<p>I have <a href="https://github.com/idjohnson/ansible-playbooks/blob/main/checkAndUpdateR53.sh">a script</a> for updating Route53, but not Google DNS (steeped.icu) nor Azure DNS (tpk.pw)</p>

<p>I also have <a href="https://github.com/idjohnson/ansible-playbooks/blob/main/testNewIp.sh">a script</a> for just testing if my IP changed and turning on a light bulb in my office</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#!/bin/bash 

MYIP="`host -4 myip.opendns.com resolver1.opendns.com | tail -n1 | sed 's/.* //' | tr -d '\n'`"
HARBOR="`host -4 harbor.freshbrewed.science resolver1.opendns.com | tail -n1 | sed 's/.* //' | tr -d '\n'`"

if [ "$MYIP" = "$HARBOR" ]; then
   echo "Strings are Equal . $MYIP matches $HARBOR"
else
   echo "IP CHANGED!  Harbor $HARBOR does not match local $MYIP"
   wget "https://kasarest.freshbrewed.science/on?devip=192.168.1.24&amp;apikey=$1"
   exit 1
fi
</code></pre></div></div>

<p>I want to start by using <a href="https://gotify.tpk.pw/">Gotify</a> for this.</p>

<p>I’ll make a new app for AWX</p>

<p><a href="/content/images/2026/03/ansible-05.png"><img src="/content/images/2026/03/ansible-05.png" alt="/content/images/2026/03/ansible-05.png" /></a></p>

<!--
<% comment %>
ABbBmjh6P_PgusL
<% endcomment %>
-->

<p>Back in February, we wrote <a href="https://freshbrewed.science/2026/02/12/gcpotel.html">about GCP OpenTelemetry</a> and in that article, to prove the idea, i created a “GotifyMe” app which we can now use. (exposed as https://notify.tpk.pw/)</p>

<p>Because this is a FastAPI based app, we can check <a href="https://notify.tpk.pw/redoc">redoc for it</a> and see the POST should be easy</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ curl -X POST -H 'Content-Type: application/json' https://notify.tpk.pw/
notify -d '{"title": "AWX: homelab IP change", "message": "IP changed from x.x.x.x to y.y.y.y", "priority": 5, "password": "xxxx"}'
{"status":"success","data":{"id":59,"appid":2,"message":"IP changed from x.x.x.x to y.y.y.y","title":"AWX: homelab IP change","priority":5,"date":"2026-03-24T11:57:01.32987876Z"}}
</code></pre></div></div>

<p>And i can see that fired through</p>

<p><a href="/content/images/2026/03/ansible-06.png"><img src="/content/images/2026/03/ansible-06.png" alt="/content/images/2026/03/ansible-06.png" /></a></p>

<p>I’ll update the quick test playbook which really hasn’t been used in a long time</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/ansible-playbooks$ cat testNewIp.sh
#!/bin/bash

export NOTIFYPASS=$1

MYIP="`host -4 myip.opendns.com resolver1.opendns.com | tail -n1 | sed 's/.* //' | tr -d '\n'`"
HARBOR="`host -4 harbor.freshbrewed.science resolver1.opendns.com | tail -n1 | sed 's/.* //' | tr -d '\n'`"

if [ "$MYIP" = "$HARBOR" ]; then
   echo "Strings are Equal . $MYIP matches $HARBOR"

   # Notify
   curl -X POST -H 'Content-Type: application/json' https://notify.tpk.pw/notify -d "{\"title\": \"AWX: homelab IP STILL GOOD\", \\"message\": \"IP of h.f.s is $HARBOR and egress is still $MYIP\", \"priority\": 5, \"password\": \"$NOTIFYPASS\"}"
else
   echo "IP CHANGED!  Harbor $HARBOR does not match local $MYIP"

   # Notify
   curl -X POST -H 'Content-Type: application/json' https://notify.tpk.pw/notify -d "{\"title\": \"AWX: homelab IP change\", \\"message\": \"IP changed from $HARBOR to $MYIP\", \"priority\": 5, \"password\": \"$NOTIFYPASS\"}"

   # light
   wget "https://kasarest.freshbrewed.science/on?devip=192.168.1.24&amp;apikey=$1"
   exit 1
fi

builder@DESKTOP-QADGF36:~/Workspaces/ansible-playbooks$ cat testNewIp.yaml
- name: Check if IP Changed
  hosts: all

  tasks:
  - name: Transfer the script
    copy: src=testNewIp.sh dest=/tmp mode=0755

  - name: Run Script
    command: sh /tmp/testNewIp.sh 

</code></pre></div></div>

<p>I’ll sync the project for the latest playbooks</p>

<p><a href="/content/images/2026/03/ansible-07.png"><img src="/content/images/2026/03/ansible-07.png" alt="/content/images/2026/03/ansible-07.png" /></a></p>

<p>I can now add the template and just use one of the utility hosts to run it</p>

<p><a href="/content/images/2026/03/ansible-08.png"><img src="/content/images/2026/03/ansible-08.png" alt="/content/images/2026/03/ansible-08.png" /></a></p>

<p>hmm…. i messed something up</p>

<p><a href="/content/images/2026/03/ansible-09.png"><img src="/content/images/2026/03/ansible-09.png" alt="/content/images/2026/03/ansible-09.png" /></a></p>

<p>testing locally shows i definately goofed</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> ./testNewIp.sh sadfsadfasdfsadf
Strings are Equal . 76.156.69.232 matches 76.156.69.232
{"detail":[{"type":"json_invalid","loc":["body",40],"msg":"JSON decode error","input":{},"ctx":{"error":"Expecting property name enclosed in double quotes"}}]}curl: (6) Could not resolve host: "IP


curl: (6) Could not resolve host: of
curl: (6) Could not resolve host: h.f.s
</code></pre></div></div>

<p>I found and fixed an errant double backslash <code class="language-plaintext highlighter-rouge">\\</code></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ./testNewIp.sh asdfsadfsafda
Strings are Equal . 76.156.69.232 matches 76.156.69.232
{"status":"success","data":{"id":60,"appid":2,"message":"IP of h.f.s is 76.156.69.232 and egress is still 76.156.69.232","title":"AWX: homelab IP STILL GOOD","priority":5,"date":"2026-03-24T12:32:32.466743299Z"}}
</code></pre></div></div>

<p>which showed up in Gotify.</p>

<p><a href="/content/images/2026/03/ansible-10.png"><img src="/content/images/2026/03/ansible-10.png" alt="/content/images/2026/03/ansible-10.png" /></a></p>

<p>I pushed <a href="https://github.com/idjohnson/ansible-playbooks/commit/6e30d4c1a01a8e5cac3dadacc1da7437e29c2c76">the fix</a>, synced the project in AWX</p>

<p>Then tried again, which worked.</p>

<p>Lastly, I commented out the notify line as that would get really old to see every hour</p>

<p><a href="/content/images/2026/03/ansible-11.png"><img src="/content/images/2026/03/ansible-11.png" alt="/content/images/2026/03/ansible-11.png" /></a></p>

<p>Lastly, I’ll create an hourly schedule so if it changes, I’ll get notified</p>

<p><a href="/content/images/2026/03/ansible-12.png"><img src="/content/images/2026/03/ansible-12.png" alt="/content/images/2026/03/ansible-12.png" /></a></p>

<p>When saved, I can see it’s scheduled here on out</p>

<p><a href="/content/images/2026/03/ansible-13.png"><img src="/content/images/2026/03/ansible-13.png" alt="/content/images/2026/03/ansible-13.png" /></a></p>

<h2 id="doh">DOH.</h2>

<p><a href="/content/images/2026/03/ansible-14.png"><img src="/content/images/2026/03/ansible-14.png" alt="https://www.etsy.com/listing/794654622/homer-simpson-doh-the-simpsons-cross" /></a></p>

<p>I was all set to write the summary and close this out when it dawned on me - if my IP changes, there is no way notify.tpk.pw would resolve, let alone <em>gotify</em>.</p>

<p>I updated the script and playbook to use email instead (and tested locally)</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(base) builder@LuiGi:~/Workspaces/ansible-playbooks$ cat testNewIp.sh
#!/bin/bash

export NOTIFYPASS=$1
export RESENDAPI=$2
export EMAILSENDER=$3

MYIP="`host -4 myip.opendns.com resolver1.opendns.com | tail -n1 | sed 's/.* //' | tr -d '\n'`"
HARBOR="`host -4 harbor.freshbrewed.science resolver1.opendns.com | tail -n1 | sed 's/.* //' | tr -d '\n'`"

if [ "$MYIP" = "$HARBOR" ]; then
   echo "Strings are Equal . $MYIP matches $HARBOR"

   # Notify
   # curl -X POST -H 'Content-Type: application/json' https://notify.tpk.pw/notify -d "{\"title\": \"AWX: homelab IP STILL GOOD\", \"message\": \"IP of h.f.s is $HARBOR and egress is still $MYIP\", \"priority\": 5, \"password\": \"$NOTIFYPASS\"}"
   # curl -X POST 'https://api.resend.com/emails' -H 'Authorization: Bearer xxxxxxxxxxxxxxxxxxxxxxxx' -H 'Content-Type: application/json' -d "{ \"from\": \"$EMAILSENDER\", \"to\": \"isaac.johnson@gmail.com\", \"subject\": \"AWX - Egress IP stable\", \"html\": \"&lt;h1&gt;hello me&lt;/h1&gt;&lt;p&gt;The IP of $HARBOR is still $MYIP egress&lt;/p&gt;\"}"
else
   echo "IP CHANGED!  Harbor $HARBOR does not match local $MYIP"

   # Notify
   curl -X POST 'https://api.resend.com/emails' -H "Authorization: Bearer $RESENDAPI" -H 'Content-Type: application/json' -d "{ \"from\": \"$EMAILSENDER\", \"to\": \"isaac.johnson@gmail.com\", \"subject\": \"AWX - Egress IP changed\", \"html\": \"&lt;h1&gt;hello me&lt;/h1&gt;&lt;p&gt;The IP of $HARBOR (harbor.freshbrewed.science) does not match your current egress IP of $MYIP&lt;/p&gt;\"}"

   # IF THE IP Changed, this really wont work till its fixed!
   curl -X POST -H 'Content-Type: application/json' https://notify.tpk.pw/notify -d "{\"title\": \"AWX: homelab IP change\", \"message\": \"IP changed from $HARBOR to $MYIP\", \"priority\": 5, \"password\": \"$NOTIFYPASS\"}"

   # light
   wget "https://kasarest.freshbrewed.science/on?devip=192.168.1.24&amp;apikey=$1"
   exit 1
fi

(base) builder@LuiGi:~/Workspaces/ansible-playbooks$ cat testNewIp.yaml
- name: Check if IP Changed
  hosts: all

  tasks:
  - name: Transfer the script
    copy: src=testNewIp.sh dest=/tmp mode=0755

  - name: Run Script
    command: sh /tmp/testNewIp.sh   
</code></pre></div></div>

<p>I then added the keys to the Template in AWX</p>

<p><a href="/content/images/2026/03/ansible-15.png"><img src="/content/images/2026/03/ansible-15.png" alt="/content/images/2026/03/ansible-15.png" /></a></p>

<p>I let it run on schedule and it had no issues</p>

<p><a href="/content/images/2026/03/ansible-16.png"><img src="/content/images/2026/03/ansible-16.png" alt="/content/images/2026/03/ansible-16.png" /></a></p>

<h1 id="summary">Summary</h1>

<p>Today we created a new script and Ansible playbook to go to the two main control planes in my home lab (I’ll add the Pi clusters later) and update Azure Key Vault (AKV) with updated local configs.  Once that was sorted, we then moved on to creating a bash script with yq to find and parse the old entries.</p>

<p>I absconded LLMs for that work until the very end and then asked Gemini to do some cleanup on the script.  My last piece of work in all this was to detect with the egress IP changes and notify myself.  That involved upating an old script that just turned on a lightbulb to instead using my notify (Gotify) app to send a push notification to my phone.  Once working, we set that playbook to run hourly so any egress change should notify my phone within the hour.</p>

<p>Lastly, right before posting, I realized that notifications that depend on ingressing to my cluster from DNS names, which neccessarily would be out of date, would be a bad idea and added some email notifications with Resend.</p>

<p>Hopefully something in here helps others, maybe some ideas on monitors or maintenance.  Home labs can be fun, but do require a bit of occasional TLC.</p>]]></content><author><name>Isaac Johnson</name></author><category term="Gotify" /><category term="Homelab" /><category term="Ansible" /><summary type="html"><![CDATA[One of my challenges is that with rapid experimentation, especially in blasting and redoing my varied Kubernetes clusters, the management of my Kubernetes config files on all my laptops was getting a bit challenging.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://freshbrewed.science/content/images/2026/03/Gemini_Generated_Image_uk9u8uk9u8uk9u8u.png" /><media:content medium="image" url="https://freshbrewed.science/content/images/2026/03/Gemini_Generated_Image_uk9u8uk9u8uk9u8u.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Revisits: Opencode and Resend</title><link href="https://freshbrewed.science/2026/03/20/revisit.html" rel="alternate" type="text/html" title="Revisits: Opencode and Resend" /><published>2026-03-20T10:00:01+00:00</published><updated>2026-03-20T10:00:01+00:00</updated><id>https://freshbrewed.science/2026/03/20/revisit</id><content type="html" xml:base="https://freshbrewed.science/2026/03/20/revisit.html"><![CDATA[<p><a href="/2025/08/19/opencode.html">In a post</a> last year, I looked briefly at Opencode, comparing it with Claude Code, and said then, “circle back… I want to explore some of its other features like agent model and maybe MCP servers.”.  <a href="https://github.com/anomalyco/opencode">Opencode</a> is still quite active so let’s give it a shake and compare the free models to using it with a paid one like Gemini.</p>

<p>Also, <a href="/2025/09/09/sgtoses.html">last year</a> I looked at <a href="https://resend.com/">Resend</a> for sending emails.  Because it lacked SMTP, i really just use it in apps or via curl.  Let’s review how to add custom domains and see if perhaps it has some new features.</p>

<h1 id="resend">Resend</h1>

<p>Back in <a href="/2025/09/09/sgtoses.html">September 2025</a> I said goodbye to Sendgrid and revisited <a href="https://resend.com/">Resend</a>.  I just touched on it then, as well as <a href="https://freshbrewed.science/2023/10/17/Tool-Roundup.html">back in 2023</a> as I had other options in hand.</p>

<p>If you have never used Resend before, you can <a href="https://resend.com/signup">create an account</a>.  For federated IdPs they have Google and Github.</p>

<p>As I mentioned in the past, this is not just some freemium SMTP relay - they will send emails, but only via API calls.</p>

<p>So our first step is to create an API key</p>

<p><a href="/content/images/2026/03/resend-01.png"><img src="/content/images/2026/03/resend-01.png" alt="/content/images/2026/03/resend-01.png" /></a></p>

<p>I can then use that API key in an email:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ curl -X POST 'https://api.resend.com/emails' \
  -H 'Authorization: Bearer re_XXXXXXXXXXXXXXXXXXXXXXXX' \
&gt; -H 'Content-Type: application/json' \
  -d $'{
    "from": "onboarding@resend.dev",
    "to": "isaac.johnson@gmail.com",
    "subject": "Hello FB 2026!", "html": "&lt;h1&gt;hello friend&lt;/h1&gt;&lt;p&gt;Congrats on testing Resend again!&lt;/p&gt;"}'
{"id":"84dd43f8-21ef-4588-b69a-c2cab03c342f"}
</code></pre></div></div>

<p>We can see it sent right away:</p>

<p><a href="/content/images/2026/03/resend-02.png"><img src="/content/images/2026/03/resend-02.png" alt="/content/images/2026/03/resend-02.png" /></a></p>

<p>As I reviewed it today, however, I noticed a new SMTP section that was not there in the past</p>

<p><a href="/content/images/2026/03/resend-03.png"><img src="/content/images/2026/03/resend-03.png" alt="/content/images/2026/03/resend-03.png" /></a></p>

<p>Could it be? Did they add SMTP (Now I really do have a drop in replacement for Sendgrid!)</p>

<p>Let’s test with SWAKS.  I’ll add SWAKS if missing</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ sudo apt update &amp;&amp; sudo apt install swaks
[sudo: authenticate] Password:
Hit:1 https://us-central1-apt.pkg.dev/projects/antigravity-auto-updater-dev antigravity-debian InRelease
Hit:2 http://security.ubuntu.com/ubuntu questing-security InRelease
Hit:3 http://us.archive.ubuntu.com/ubuntu questing InRelease
Get:4 https://repository.mullvad.net/deb/stable stable InRelease [3,536 B]
Hit:5 http://us.archive.ubuntu.com/ubuntu questing-updates InRelease
Hit:6 http://us.archive.ubuntu.com/ubuntu questing-backports InRelease
Fetched 3,536 B in 1s (2,394 B/s)
123 packages can be upgraded. Run 'apt list --upgradable' to see them.
Notice: Skipping acquire of configured file 'main/binary-i386/Packages' as repository 'https://us-central1-apt.pkg.dev/projects/antigravity-auto-updater-dev antigravity-debian InRelease' doesn't support architecture 'i386'
Installing:
  swaks

Installing dependencies:
  libdigest-bubblebabble-perl  libnet-dns-perl         libsocket6-perl
  libdigest-hmac-perl          libnet-dns-sec-perl
  libio-socket-inet6-perl      libperl4-corelibs-perl

Suggested packages:
  libauthen-ntlm-perl  perl-doc

Summary:
  Upgrading: 0, Installing: 8, Removing: 0, Not Upgrading: 123
  Download size: 558 kB
  Space needed: 1,754 kB / 718 GB available

Continue? [Y/n] Y
Get:1 http://us.archive.ubuntu.com/ubuntu questing/main amd64 libdigest-bubblebabble-perl all 0.02-3 [6,724 B]
Get:2 http://us.archive.ubuntu.com/ubuntu questing/main amd64 libdigest-hmac-perl all 1.05+dfsg-1 [8,416 B]
Get:3 http://us.archive.ubuntu.com/ubuntu questing/main amd64 libsocket6-perl amd64 0.29-3build4 [17.6 kB]
Get:4 http://us.archive.ubuntu.com/ubuntu questing/main amd64 libio-socket-inet6-perl all 2.73-1 [14.7 kB]
Get:5 http://us.archive.ubuntu.com/ubuntu questing/main amd64 libnet-dns-perl all 1.50-1ubuntu1 [340 kB]
Get:6 http://us.archive.ubuntu.com/ubuntu questing/main amd64 libnet-dns-sec-perl amd64 1.26-1build1 [40.6 kB]
Get:7 http://us.archive.ubuntu.com/ubuntu questing/main amd64 libperl4-corelibs-perl all 0.005-1 [38.1 kB]
Get:8 http://us.archive.ubuntu.com/ubuntu questing/universe amd64 swaks all 20240103.0-2 [92.4 kB]
Fetched 558 kB in 1s (409 kB/s)
Selecting previously unselected package libdigest-bubblebabble-perl.
(Reading database ... 342210 files and directories currently installed.)
Preparing to unpack .../0-libdigest-bubblebabble-perl_0.02-3_all.deb ...
Unpacking libdigest-bubblebabble-perl (0.02-3) ...
Selecting previously unselected package libdigest-hmac-perl.
Preparing to unpack .../1-libdigest-hmac-perl_1.05+dfsg-1_all.deb ...
Unpacking libdigest-hmac-perl (1.05+dfsg-1) ...
Selecting previously unselected package libsocket6-perl.
Preparing to unpack .../2-libsocket6-perl_0.29-3build4_amd64.deb ...
Unpacking libsocket6-perl (0.29-3build4) ...
Selecting previously unselected package libio-socket-inet6-perl.
Preparing to unpack .../3-libio-socket-inet6-perl_2.73-1_all.deb ...
Unpacking libio-socket-inet6-perl (2.73-1) ...
Selecting previously unselected package libnet-dns-perl.
Preparing to unpack .../4-libnet-dns-perl_1.50-1ubuntu1_all.deb ...         ]
Unpacking libnet-dns-perl (1.50-1ubuntu1) ...                                ]
Selecting previously unselected package libnet-dns-sec-perl.                    ]
Preparing to unpack .../5-libnet-dns-sec-perl_1.26-1build1_amd64.deb ...           ]
Unpacking libnet-dns-sec-perl (1.26-1build1) ...                                     ]
Selecting previously unselected package libperl4-corelibs-perl.
Preparing to unpack .../6-libperl4-corelibs-perl_0.005-1_all.deb ...                   ]
Unpacking libperl4-corelibs-perl (0.005-1) ...                                          ]
Selecting previously unselected package swaks.                                            ]
Preparing to unpack .../7-swaks_20240103.0-2_all.deb ...                                     ]
Unpacking swaks (20240103.0-2) ...
Setting up libperl4-corelibs-perl (0.005-1) ...
Setting up libdigest-hmac-perl (1.05+dfsg-1) ...
Setting up libsocket6-perl (0.29-3build4) ...
Setting up swaks (20240103.0-2) ...
Setting up libdigest-bubblebabble-perl (0.02-3) ...
Setting up libnet-dns-perl (1.50-1ubuntu1) ...
Setting up libio-socket-inet6-perl (2.73-1) ...██████████████████████████▎                 ]  Setting up libnet-dns-sec-perl (1.26-1build1) ...
Processing triggers for man-db (2.13.1-1) ...
</code></pre></div></div>

<p>Let’s give it a try (I’ll remove parts that show my auth in output)</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ swaks --to isaac.johnson@gmail.com --from isaac@steeped.space --server smtp.resend.com:587 --auth LOGIN --tls --auth-user resend --auth-password 're_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
=== Trying smtp.resend.com:587...
=== Connected to smtp.resend.com.
&lt;-  220 Resend SMTP Relay ESMTP
 -&gt; EHLO LuiGi
&lt;-  250-Resend SMTP Relay Nice to meet you, [10.0.31.199]
&lt;-  250-PIPELINING
&lt;-  250-8BITMIME
&lt;-  250-SMTPUTF8
&lt;-  250-AUTH PLAIN LOGIN
&lt;-  250-STARTTLS
&lt;-  250 SIZE 41943040
 -&gt; STARTTLS
&lt;-  220 Ready to start TLS
=== TLS started with cipher TLSv1.3:TLS_AES_256_GCM_SHA384:256
=== TLS client certificate not requested and not sent
=== TLS no client certificate set
=== TLS peer[0]   subject=[/CN=*.resend.com]
===               commonName=[*.resend.com], subjectAltName=[DNS:*.apps.resend.com, DNS:*.resend.com, DNS:resend.com] notAfter=[2026-03-31T14:56:52Z]
=== TLS peer[1]   subject=[/CN=*.resend.com]
===               commonName=[*.resend.com], subjectAltName=[DNS:*.apps.resend.com, DNS:*.resend.com, DNS:resend.com] notAfter=[2026-03-31T14:56:52Z]
=== TLS peer[2]   subject=[/C=US/O=Let's Encrypt/CN=E8]
===               commonName=[E8], subjectAltName=[] notAfter=[2027-03-12T23:59:59Z]
=== TLS peer certificate passed CA verification, passed host verification (using host smtp.resend.com to verify)
 ~&gt; EHLO LuiGi
&lt;~  250-Resend SMTP Relay Nice to meet you, [10.0.31.199]
&lt;~  250-PIPELINING
&lt;~  250-8BITMIME
&lt;~  250-SMTPUTF8
&lt;~  250-AUTH PLAIN LOGIN
&lt;~  250 SIZE 41943040
 ~&gt; AUTH LOGIN
&lt;~  334 xxxxxxxxxxxxxxxxxxxxx
&lt;~  334 xxxxxxxxxxxxxxxxxxxxx
&lt;~  235 Authentication successful
 ~&gt; MAIL FROM:&lt;isaac@steeped.space&gt;
&lt;~  250 Accepted
 ~&gt; RCPT TO:&lt;isaac.johnson@gmail.com&gt;
&lt;~  250 Accepted
 ~&gt; DATA
&lt;~  354 End data with &lt;CR&gt;&lt;LF&gt;.&lt;CR&gt;&lt;LF&gt;
 ~&gt; Date: Fri, 20 Mar 2026 07:39:16 -0500
 ~&gt; To: isaac.johnson@gmail.com
 ~&gt; From: isaac@steeped.space
 ~&gt; Subject: test Fri, 20 Mar 2026 07:39:16 -0500
 ~&gt; Message-Id: &lt;20260320073916.039869@LuiGi&gt;
 ~&gt; X-Mailer: swaks v20240103.0 jetmore.org/john/code/swaks/
 ~&gt;
 ~&gt; This is a test mailing
 ~&gt;
 ~&gt;
 ~&gt; .
&lt;~  250 f23f9f1d-4a40-4dc3-bdd5-820130ab4e07
 ~&gt; QUIT
&lt;~  221 Bye
=== Connection closed with remote host.
</code></pre></div></div>

<p>However, I never saw it come through.  In fairness, I let steeped.space go so perhaps Google blocked it?</p>

<p>I then noticed Resend blocked it now</p>

<p><a href="/content/images/2026/03/resend-04.png"><img src="/content/images/2026/03/resend-04.png" alt="/content/images/2026/03/resend-04.png" /></a></p>

<p>I delete the expired and started the process to add a new one</p>

<p><a href="/content/images/2026/03/resend-05.png"><img src="/content/images/2026/03/resend-05.png" alt="/content/images/2026/03/resend-05.png" /></a></p>

<p>I’ll need to set some records for verification</p>

<p><a href="/content/images/2026/03/resend-06.png"><img src="/content/images/2026/03/resend-06.png" alt="/content/images/2026/03/resend-06.png" /></a></p>

<p>While it said it might take a day to verify, it took less than 30m to come back verified</p>

<p><a href="/content/images/2026/03/resend-07.png"><img src="/content/images/2026/03/resend-07.png" alt="/content/images/2026/03/resend-07.png" /></a></p>

<p>I’ll give it a shot</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ swaks --to isaac@freshbrewed.science \
--from isaac@steeped.space \
--server smtp.resend.com:587 \
--auth LOGIN --tls \
--auth-user resend \
--auth-password 're_xxxxxxxxxxxxxxxxxxxxx'
</code></pre></div></div>

<p>And it worked just fine!</p>

<p><a href="/content/images/2026/03/resend-08.png"><img src="/content/images/2026/03/resend-08.png" alt="/content/images/2026/03/resend-08.png" /></a></p>

<p>We can also see that as a positive result in the logs of Resend</p>

<p><a href="/content/images/2026/03/resend-09.png"><img src="/content/images/2026/03/resend-09.png" alt="/content/images/2026/03/resend-09.png" /></a></p>

<h2 id="paid-plans">Paid plans</h2>

<p>Resend, sadly, just has a $0, US$20, and US$90 plan available today</p>

<p><a href="/content/images/2026/03/resend-10.png"><img src="/content/images/2026/03/resend-10.png" alt="/content/images/2026/03/resend-10.png" /></a></p>

<p>I really wish vendors would embrace the home-lab type people that just want some more domains but are fine with lesser limits.  I would be keen on a $5 or perhaps even $10 a month plan, but not $20 - just for sending mails.</p>

<h1 id="opencode">Opencode</h1>

<p>Back in <a href="/2025/08/19/opencode.html">August this past year</a> I looked at <a href="https://github.com/anomalyco/opencode">Opencode</a>.  It worked okay with Gemini 2.5 but I was leaning into Qwen3 and Gemini CLI at the time.</p>

<p>It’s still quite active - let’s give it another shot.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ curl -fsSL https://opencode.ai/install | bash
</code></pre></div></div>

<p><a href="/content/images/2026/03/resend-11.png"><img src="/content/images/2026/03/resend-11.png" alt="/content/images/2026/03/resend-11.png" /></a></p>

<p>Let’s see if we can get it to help build an agent skills markdown file for python work:</p>

<video muted="" controls="">
    <source src="/content/images/2026/03/resend-12.mp4" type="video/mp4" />
</video>

<p>So, as you saw, using the free MiniMax M2.5 Free model it took a good 10m - but free is free.</p>

<p>If we set a GEMINI API Key, we can use the Google models as well</p>

<p><a href="/content/images/2026/03/opencode-13.png"><img src="/content/images/2026/03/opencode-13.png" alt="/content/images/2026/03/opencode-13.png" /></a></p>

<p>One issue I noticed is the new “SKILL” made is it omitted the YAML block at the top of the SKILL.md so it doesn’t show up when list skills.</p>

<p>I’ll add it myself to ~/.agents/skills/python-app-setup/SKILL.md</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>---
name: my-python-app-skill
description: When I ask for my python process, or my python skill, follow these rules.  Use this skill whenCreating a new Python application, Scaffolding a Python project, Setting up a new FastAPI or FastMCP project, or Initializing a Python project from scratch
metadata:
  copyright: Copyright Freshbrewed. 2026
  version: "0.0.1"
---
</code></pre></div></div>

<p>Now when I fire up OpenCode I can see it listed</p>

<p><a href="/content/images/2026/03/opencode-14.png"><img src="/content/images/2026/03/opencode-14.png" alt="/content/images/2026/03/opencode-14.png" /></a></p>

<p>However, with Gemini 3 flash, it got hung up on some loops - it was working, just kind of got a bit stuck</p>

<video muted="" controls="">
    <source src="/content/images/2026/03/opencode-15.mp4" type="video/mp4" />
</video>

<p>I got MiniMax working, but of course as a free model its quite slow</p>

<p><a href="/content/images/2026/03/opencode-16.png"><img src="/content/images/2026/03/opencode-16.png" alt="/content/images/2026/03/opencode-16.png" /></a></p>

<p>I fired up the app it created</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ uvicorn app.main:app --reload
</code></pre></div></div>

<p>I mean, the layout looks fine but now way to add notes which is a bit of an issue</p>

<p><a href="/content/images/2026/03/opencode-17.png"><img src="/content/images/2026/03/opencode-17.png" alt="/content/images/2026/03/opencode-17.png" /></a></p>

<p>I switched to Gemini Pro to try and fix things.  One thing we’ll note in Opencode is the running cost in the upper right.  This can be handy if really focused on spend</p>

<p><a href="/content/images/2026/03/opencode-18.png"><img src="/content/images/2026/03/opencode-18.png" alt="/content/images/2026/03/opencode-18.png" /></a></p>

<p>This is important as using the Pro model to fix my ask (and I didn’t need to - Flash would have been fine) would cost me $0.29</p>

<p><a href="/content/images/2026/03/opencode-19.png"><img src="/content/images/2026/03/opencode-19.png" alt="/content/images/2026/03/opencode-19.png" /></a></p>

<p>Here we can see it in action!</p>

<video muted="" controls="">
    <source src="/content/images/2026/03/opencode-20.mp4" type="video/mp4" />
</video>

<p>There is a Dockerfile, which was a requirement from the skill we built</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cat Dockerfile
# 411
FROM python:3.11-slim

WORKDIR /app

COPY --from=build /app/dist /app

EXPOSE 8000

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
</code></pre></div></div>

<p>And there exists a SYSTEM.md with a diagram as I desired for the architecture</p>

<p><a href="/content/images/2026/03/opencode-20.png"><img src="/content/images/2026/03/opencode-20.png" alt="/content/images/2026/03/opencode-20.png" /></a></p>

<p>Of course, sharing is caring, so publishing to Github is next.</p>

<p>You can find the repo at <a href="https://github.com/idjohnson/simpleTodo">https://github.com/idjohnson/simpleTodo</a>.</p>

<p>Where the mermaid renders in browser without issue</p>

<p><a href="/content/images/2026/03/opencode-21.png"><img src="/content/images/2026/03/opencode-21.png" alt="/content/images/2026/03/opencode-21.png" /></a></p>

<h1 id="summary">Summary</h1>

<p>Today we looked at <a href="https://resend.com/">Resend</a> again and were pleasantly surprised to see they added SMTP support. I added a new domain and tested sending with SWAKS.  We wrapped by discussing costs for their paid tier</p>

<p>Then we circled back to <a href="https://github.com/anomalyco/opencode">Opencode</a> to give it a good try and use it to build a skill, then use the skill in an agentic flow to build, of course, a to-do app.  We wrapped it up by fixing it with Gemini Pro, looking at costs, and pushing the resulting app <a href="https://github.com/idjohnson/simpleTodo/">to Github</a>.</p>

<p>I didn’t dive into too much as I don’t want to always promote Google uber alles.. but one of the biggest reasons I lean in on Gemini CLI over tools like Codex and Opencode is how it <em>caches</em> tokens.  HUGE savings (and that matters to me).  So as you saw, fixing a hole in the app was quick with Gemini Pro in Opencode, but with no token caching, that cost US$0.29 (no worries, i had some credits to cover that).  I cannot be certain, but my experience with caching in the past would suggest i would have spent a third of that had I used Gemini CLI.</p>

<p>I do think the use of Skills is a topic we need to followup on. Just this last night the topic of MCP vs Skills came up again and there was a fun passionate debate on usage and pros/cons.  Stay tuned for my take on that soon.</p>]]></content><author><name>Isaac Johnson</name></author><category term="Opensource" /><category term="Opencode" /><category term="Minimax" /><category term="Genai" /><category term="Gemini" /><category term="Resend" /><category term="Email" /><summary type="html"><![CDATA[In a post last year, I looked briefly at Opencode, comparing it with Claude Code, and said then, “circle back… I want to explore some of its other features like agent model and maybe MCP servers.”. Opencode is still quite active so let’s give it a shake and compare the free models to using it with a paid one like Gemini.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://freshbrewed.science/content/images/2026/03/Gemini_Generated_Image_sq5h5xsq5h5xsq5h.png" /><media:content medium="image" url="https://freshbrewed.science/content/images/2026/03/Gemini_Generated_Image_sq5h5xsq5h5xsq5h.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">K8s VPAs now GA in 1.35</title><link href="https://freshbrewed.science/2026/03/18/vpas.html" rel="alternate" type="text/html" title="K8s VPAs now GA in 1.35" /><published>2026-03-18T12:55:01+00:00</published><updated>2026-03-18T12:55:01+00:00</updated><id>https://freshbrewed.science/2026/03/18/vpas</id><content type="html" xml:base="https://freshbrewed.science/2026/03/18/vpas.html"><![CDATA[<p>I had bookmarked <a href="https://thenewstack.io/kubernetes-vpa-inplace-resize/">this article on NewStack</a> about <a href="https://kubernetes.io/docs/concepts/workloads/autoscaling/vertical-pod-autoscale/">VerticalPodAutoscaling</a> finally going GA.  It’s been around for a while, and can be really useful with StatefulSets.  I wanted to try in on Deployments (Replicasets) and see how it really works.</p>

<p>My “test” cluster was on v1.31.9 so it seemed a simple K3s upgrade aught to sort me out…</p>

<h1 id="upgrading-k3s">Upgrading K3s</h1>

<p>Presently my test cluster is running v1.31.9 which is good, but we really want a newer v1.35 to properly test out the built-in vertical pod autoscaling</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get nodes
NAME                  STATUS   ROLES                  AGE    VERSION
builder-macbookpro2   Ready    &lt;none&gt;                 265d   v1.31.9+k3s1
isaac-macbookair      Ready    control-plane,master   265d   v1.31.9+k3s1
isaac-macbookpro      Ready    &lt;none&gt;                 265d   v1.31.9+k3s1
</code></pre></div></div>

<p>We’ll follow the <a href="https://docs.k3s.io/upgrades/automated">automated upgrade</a> docs</p>

<p>I’ll first apply the upgrade controller</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl apply -f https://github.com/rancher/system-upgrade-controller/releases/latest/download/crd.yaml -f https://github.com/rancher/system-upgrade-controller/releases/latest/download/system-upgrade-controller.yaml
customresourcedefinition.apiextensions.k8s.io/plans.upgrade.cattle.io created
namespace/system-upgrade created
serviceaccount/system-upgrade created
role.rbac.authorization.k8s.io/system-upgrade-controller created
clusterrole.rbac.authorization.k8s.io/system-upgrade-controller created
clusterrole.rbac.authorization.k8s.io/system-upgrade-controller-drainer created
rolebinding.rbac.authorization.k8s.io/system-upgrade created
clusterrolebinding.rbac.authorization.k8s.io/system-upgrade created
clusterrolebinding.rbac.authorization.k8s.io/system-upgrade-drainer created
configmap/default-controller-env created
deployment.apps/system-upgrade-controller created
</code></pre></div></div>

<p>Then create an upgrade manifest <em>(and yes, there is a typo I’ll figure out soon)</em></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">$ cat updatek3s.yaml</span>
<span class="c1"># Server plan</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">upgrade.cattle.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Plan</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">server-plan</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">system-upgrade</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">concurrency</span><span class="pi">:</span> <span class="m">1</span>
  <span class="na">cordon</span><span class="pi">:</span> <span class="no">true</span>
  <span class="na">version</span><span class="pi">:</span> <span class="s">v.1.35.1+k3s1</span>
  <span class="na">nodeSelector</span><span class="pi">:</span>
    <span class="na">matchExpressions</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">key</span><span class="pi">:</span> <span class="s">node-role.kubernetes.io/control-plane</span>
      <span class="na">operator</span><span class="pi">:</span> <span class="s">In</span>
      <span class="na">values</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">true"</span>
  <span class="na">serviceAccountName</span><span class="pi">:</span> <span class="s">system-upgrade</span>
  <span class="na">upgrade</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">rancher/k3s-upgrade</span>
  <span class="na">channel</span><span class="pi">:</span> <span class="s">https://update.k3s.io/v1-release/channels/stable</span>
<span class="nn">---</span>
<span class="c1"># Agent plan</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">upgrade.cattle.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Plan</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">agent-plan</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">system-upgrade</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">concurrency</span><span class="pi">:</span> <span class="m">1</span>
  <span class="na">cordon</span><span class="pi">:</span> <span class="no">true</span>
  <span class="na">version</span><span class="pi">:</span> <span class="s">v.1.35.1+k3s1</span>
  <span class="na">nodeSelector</span><span class="pi">:</span>
    <span class="na">matchExpressions</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">key</span><span class="pi">:</span> <span class="s">node-role.kubernetes.io/control-plane</span>
      <span class="na">operator</span><span class="pi">:</span> <span class="s">DoesNotExist</span>
  <span class="na">prepare</span><span class="pi">:</span>
    <span class="na">args</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">prepare</span>
    <span class="pi">-</span> <span class="s">server-plan</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">rancher/k3s-upgrade</span>
  <span class="na">serviceAccountName</span><span class="pi">:</span> <span class="s">system-upgrade</span>
  <span class="na">upgrade</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">rancher/k3s-upgrade</span>
  <span class="na">channel</span><span class="pi">:</span> <span class="s">https://update.k3s.io/v1-release/channels/stable</span>
</code></pre></div></div>

<p>I’ll apply the upgrade plan</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl apply -f ./updatek3s.yaml
plan.upgrade.cattle.io/server-plan created
plan.upgrade.cattle.io/agent-plan created
</code></pre></div></div>

<p>and generally this moves fast so I’ll check my nodes</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get nodes
NAME                  STATUS                     ROLES                  AGE    VERSION
builder-macbookpro2   Ready                      &lt;none&gt;                 265d   v1.31.9+k3s1
isaac-macbookair      Ready,SchedulingDisabled   control-plane,master   265d   v1.31.9+k3s1
isaac-macbookpro      Ready                      &lt;none&gt;                 265d   v1.31.9+k3s1
</code></pre></div></div>

<p>hmm.. nothing is moving… let’s look at the plan status</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl -n system-upgrade get plans -o wide
NAME          IMAGE                 CHANNEL                                            VERSION         COMPLETE   MESSAGE   APPLYING
agent-plan    rancher/k3s-upgrade   https://update.k3s.io/v1-release/channels/stable   v.1.35.1+k3s1   False                ["isaac-macbookpro"]
server-plan   rancher/k3s-upgrade   https://update.k3s.io/v1-release/channels/stable   v.1.35.1+k3s1   False                ["isaac-macbookair"]
</code></pre></div></div>

<p>Ah! I used <code class="language-plaintext highlighter-rouge">v.1.35.1+k3s1</code> instead of <code class="language-plaintext highlighter-rouge">v1.35.1+k3s1</code>! DOH!</p>

<p>At first I tried just applying a fixed copy</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cat updatek3s.yaml
# Server plan
apiVersion: upgrade.cattle.io/v1
kind: Plan
metadata:
  name: server-plan
  namespace: system-upgrade
spec:
  concurrency: 1
  cordon: true
  version: v1.35.1+k3s1
  nodeSelector:
    matchExpressions:
    - key: node-role.kubernetes.io/control-plane
      operator: In
      values:
      - "true"
  serviceAccountName: system-upgrade
  upgrade:
    image: rancher/k3s-upgrade
  channel: https://update.k3s.io/v1-release/channels/stable
---
# Agent plan
apiVersion: upgrade.cattle.io/v1
kind: Plan
metadata:
  name: agent-plan
  namespace: system-upgrade
spec:
  concurrency: 1
  cordon: true
  version: v1.35.1+k3s1
  nodeSelector:
    matchExpressions:
    - key: node-role.kubernetes.io/control-plane
      operator: DoesNotExist
  prepare:
    args:
    - prepare
    - server-plan
    image: rancher/k3s-upgrade
  serviceAccountName: system-upgrade
  upgrade:
    image: rancher/k3s-upgrade
  channel: https://update.k3s.io/v1-release/channels/stable
$ kubectl apply -f ./updatek3s.yaml
plan.upgrade.cattle.io/server-plan configured
plan.upgrade.cattle.io/agent-plan configured
$ kubectl -n system-upgrade get plans -o wide
NAME          IMAGE                 CHANNEL                                            VERSION        COMPLETE   MESSAGE   APPLYING
agent-plan    rancher/k3s-upgrade   https://update.k3s.io/v1-release/channels/stable   v1.35.1+k3s1   False                ["isaac-macbookpro"]
server-plan   rancher/k3s-upgrade   https://update.k3s.io/v1-release/channels/stable   v1.35.1+k3s1   False                ["isaac-macbookair"]
</code></pre></div></div>

<p>But I saw nothing was moving.  So I removed the plan then re-added it</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl delete -f ./updatek3s.yaml
plan.upgrade.cattle.io "server-plan" deleted
plan.upgrade.cattle.io "agent-plan" deleted

$ kubectl -n system-upgrade get plans -o wide
No resources found in system-upgrade namespace.

$ kubectl apply -f ./updatek3s.yaml
plan.upgrade.cattle.io/server-plan created
plan.upgrade.cattle.io/agent-plan created

</code></pre></div></div>

<p>Now I’ll check my nodes</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get nodes
NAME                  STATUS                     ROLES                  AGE    VERSION
builder-macbookpro2   Ready                      &lt;none&gt;                 265d   v1.31.9+k3s1
isaac-macbookair      Ready,SchedulingDisabled   control-plane,master   265d   v1.31.9+k3s1
isaac-macbookpro      Ready                      &lt;none&gt;                 265d   v1.31.9+k3s1
</code></pre></div></div>

<p>Seeing the “SchedulingDisabled” was a good sign.</p>

<p>I checked again</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get nodes
NAME                  STATUS   ROLES                  AGE    VERSION
builder-macbookpro2   Ready    &lt;none&gt;                 265d   v1.31.9+k3s1
isaac-macbookair      Ready    control-plane,master   265d   v1.35.1+k3s1
isaac-macbookpro      Ready    &lt;none&gt;                 265d   v1.31.9+k3s1
</code></pre></div></div>

<p>and the master was already upgraded</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl -n system-upgrade get plans -o wide
NAME          IMAGE                 CHANNEL                                            VERSION        COMPLETE   MESSAGE   APPLYING
agent-plan    rancher/k3s-upgrade   https://update.k3s.io/v1-release/channels/stable   v1.35.1+k3s1   False                ["builder-macbookpro2"]
server-plan   rancher/k3s-upgrade   https://update.k3s.io/v1-release/channels/stable   v1.35.1+k3s1   True
</code></pre></div></div>

<p>The nodes, however</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl -n system-upgrade get plans -o wide
NAME          IMAGE                 CHANNEL                                            VERSION        COMPLETE   MESSAGE   APPLYING
agent-plan    rancher/k3s-upgrade   https://update.k3s.io/v1-release/channels/stable   v1.35.1+k3s1   False                ["builder-macbookpro2"]
server-plan   rancher/k3s-upgrade   https://update.k3s.io/v1-release/channels/stable   v1.35.1+k3s1   True

$ kubectl get nodes
NAME                  STATUS                        ROLES                  AGE    VERSION
builder-macbookpro2   NotReady,SchedulingDisabled   &lt;none&gt;                 265d   v1.31.9+k3s1
isaac-macbookair      Ready                         control-plane,master   265d   v1.35.1+k3s1
isaac-macbookpro      Ready                         &lt;none&gt;                 265d   v1.31.9+k3s1
</code></pre></div></div>

<p>were taking a lot longer (granted they are very very old hosts).</p>

<p>I then pivoted to manually upgrading.</p>

<p>I would get the token and config</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@builder-MacBookPro2:/var/lib/rancher/k3s# cat /etc/systemd/system/k3s-agent.service.env
K3S_TOKEN='K10a18c07f24914cbe61277875fcb3e477fc063ada7cf69312f515909fa08b7dbf7::server:8ae66788f324cc82d03aa0171b9f59c4'
K3S_URL='https://192.168.1.77:6443'
</code></pre></div></div>

<p>Now I can use that to launch</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION=v1.35.1+k3s1 K3S_URL=https://192.168.1.77:6443 K3S_TOKEN=K10a18c07f24914cbe61277875fcb3e477fc063ada7cf69312f515909fa08b7dbf7::server:8ae66788f324cc82d03aa0171b9f59c4 sh -
[INFO]  Using v1.35.1+k3s1 as release
[INFO]  Downloading hash https://github.com/k3s-io/k3s/releases/download/v1.35.1%2Bk3s1/sha256sum-amd64.txt
[INFO]  Skipping binary downloaded, installed k3s matches hash
[INFO]  Skipping installation of SELinux RPM
[INFO]  Skipping /usr/local/bin/kubectl symlink to k3s, already exists
[INFO]  Skipping /usr/local/bin/crictl symlink to k3s, already exists
[INFO]  Skipping /usr/local/bin/ctr symlink to k3s, command exists in PATH at /usr/bin/ctr
[INFO]  Creating killall script /usr/local/bin/k3s-killall.sh
[INFO]  Creating uninstall script /usr/local/bin/k3s-agent-uninstall.sh
[INFO]  env: Creating environment file /etc/systemd/system/k3s-agent.service.env
[INFO]  systemd: Creating service file /etc/systemd/system/k3s-agent.service
[INFO]  systemd: Enabling k3s-agent unit
Created symlink /etc/systemd/system/multi-user.target.wants/k3s-agent.service → /etc/systemd/system/k3s-agent.service.
[INFO]  systemd: Starting k3s-agent
Job for k3s-agent.service failed because the service did not take the steps required by its unit configuration.
See "systemctl status k3s-agent.service" and "journalctl -xe" for details.
</code></pre></div></div>

<p>But that still didn’t seem to work</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get nodes
NAME                  STATUS                        ROLES                  AGE    VERSION
builder-macbookpro2   NotReady,SchedulingDisabled   &lt;none&gt;                 265d   v1.31.9+k3s1
isaac-macbookair      Ready                         control-plane,master   265d   v1.35.1+k3s1
isaac-macbookpro      Ready                         &lt;none&gt;                 265d   v1.31.9+k3s1


$ sudo systemctl stop k3s-agent
$ sudo systemctl start k3s-agent
</code></pre></div></div>

<p>That failed</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get nodes
NAME                  STATUS                        ROLES                  AGE    VERSION
builder-macbookpro2   NotReady,SchedulingDisabled   &lt;none&gt;                 265d   v1.31.9+k3s1
isaac-macbookair      Ready                         control-plane,master   265d   v1.35.1+k3s1
isaac-macbookpro      Ready                         &lt;none&gt;                 265d   v1.31.9+k3s1
</code></pre></div></div>

<h2 id="ubuntu-2004-focal-and-kubernetes-135">Ubuntu 20.04 Focal and Kubernetes 1.35</h2>

<p>It took me a whole day (delaying this post).  I tried ansible playbooks</p>

<p>I could reset the whole cluster to v1.31.5 without issue.  However, trying v1.35.1 failed with errors after hours…</p>

<p><a href="/content/images/2026/03/k3supgrade-01.png"><img src="/content/images/2026/03/k3supgrade-01.png" alt="/content/images/2026/03/k3supgrade-01.png" /></a></p>

<p>Timing out on the nodes</p>

<p><a href="/content/images/2026/03/k3supgrade-02.png"><img src="/content/images/2026/03/k3supgrade-02.png" alt="/content/images/2026/03/k3supgrade-02.png" /></a></p>

<p>I then tried manually but it refused to start and join the master.  The token was right, the IP of the master instance was right.</p>

<p>Journalctl showed a fatal error at startup saying “k3s kubelet is configured to not run on a host using cgroup v1”.</p>

<p>I then checked my hosts.  Master was on an old Macbook Air running Ubuntu 22.04 but the old old old Macbook Pros were on Ubuntu 20.04 (focal).  Perhaps this is just a case that the OS is too out of date.</p>

<p>For more hours I tried upgrading… They just refused to find a new release.</p>

<p>The usual pattern of:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ sudo apt update
$ sudo apt upgrade
$ sudo do-release-upgrade
</code></pre></div></div>

<p>Would just return “No new release found”.</p>

<p>I then fired up ‘deep thinking’ Gemini to see if it had any other ideas (my next step would be to burn a thumb drive with Jammy and just do it all over - but then i have to pull laptops from a rack and that’s another big hassle)</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@isaac-MacBookPro:~$ lsb_release -a

No LSB modules are available.

Distributor ID: Ubuntu

Description:    Ubuntu 20.04.6 LTS

Release:        20.04

Codename:       focal



Yet, it refuses to upgrade:



builder@isaac-MacBookPro:~$ sudo do-release-upgrade -d

Checking for a new Ubuntu release

Upgrades to the development release are only

available from the latest supported release.



What can i do to force it to upgrade to Ubuntu 22?
</code></pre></div></div>

<p>Gemini proposed two paths - so I tried both - each on a different host:</p>

<h3 id="1-force-method-via-dist-upgrade">1. “Force” Method via “dist-upgrade”</h3>

<p>First, we tell the system to look at Jammy instead of Focal Fossa (20.04)</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ sudo sed -i 's/focal/jammy/g' /etc/apt/sources.list
</code></pre></div></div>

<p>Then run each of these steps</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ sudo apt update
$ sudo apt upgrade --without-new-pkgs -y
$ sudo apt full-upgrade -y
</code></pre></div></div>

<h3 id="2-fix-the-do-release-upgrade">2. “Fix” the do-release-upgrade</h3>

<p>We can force the upgrader tool to reinstall itself and hope that clears out the cobwebs</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ sudo apt install --reinstall ubuntu-release-upgrader-core
$ sudo do-release-upgrade
</code></pre></div></div>

<p>You have to babysit it a bit - this killed my overnight updates because one timed out asking about the sudoers and the other timed out asking about Firefox being configured for snap.  I really wish they had something like “–yolo” or “–fuggoff-and-just-do-it”</p>

<p>Because in both my cases they timed out, closing the SSH session after a while, I had to force kill stuck apt processes and then fix before trying again</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ sudo kill &lt;apt process&gt;
$ sudo dpkg --configure -a
$ sudo apt --fix-broken install
</code></pre></div></div>

<h2 id="fubar">FUBAR</h2>

<p>In the end, on reboot, both laptops were in a failed state, refusing to boot into a desktop - caught in a recovery mode with little options (as it had no network nor clue how to resolve)</p>

<p>So I was forced to do it the full blast way - use a thumb drive</p>

<p><a href="/content/images/2026/03/k3supgrade-30.jpg"><img src="/content/images/2026/03/k3supgrade-30.jpg" alt="/content/images/2026/03/k3supgrade-30.jpg" /></a></p>

<p>Also, with 24.04, the old “gnome-tweaks” method of preventing sleep on lid closure is gone so you have t use the logind.conf method (see: <a href="https://askubuntu.com/questions/1427093/how-to-make-ubuntu-to-not-suspend-when-laptop-lid-is-closed">askubuntu post</a>)</p>

<p>And i always forget there are crazy people out there that use Nano.. ick.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@builder-MacBookPro8-1:~$ sudo update-alternatives --config editor
There are 4 choices for the alternative editor (providing /usr/bin/editor).

  Selection    Path                Priority   Status
------------------------------------------------------------
* 0            /bin/nano            40        auto mode
  1            /bin/ed             -100       manual mode
  2            /bin/nano            40        manual mode
  3            /usr/bin/vim.basic   30        manual mode
  4            /usr/bin/vim.tiny    15        manual mode

Press &lt;enter&gt; to keep the current choice[*], or type selection number: 3
update-alternatives: using /usr/bin/vim.basic to provide /usr/bin/editor (editor) in manual mode
</code></pre></div></div>

<p>Once live, I did the usual steps of adding docker and openssh-server.  But next I wanted to try and rejoin the host to the control-plane:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@builder-MacBookPro8-1:~$ curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION=v1.35.1+k3s1 K3S_URL=https://192.168.1.77:6443 K3S_TOKEN=K1092e2cff3c646a253da6a2aa177d0edd6626fc1c553d379cda72edf0035742bcb::server:7756fd74f3b4d1b739ab50ce337ef220 sh -
[INFO]  Using v1.35.1+k3s1 as release
[INFO]  Downloading hash https://github.com/k3s-io/k3s/releases/download/v1.35.1%2Bk3s1/sha256sum-amd64.txt
[INFO]  Downloading binary https://github.com/k3s-io/k3s/releases/download/v1.35.1%2Bk3s1/k3s
[INFO]  Verifying binary download
[INFO]  Installing k3s to /usr/local/bin/k3s
[INFO]  Skipping installation of SELinux RPM
[INFO]  Creating /usr/local/bin/kubectl symlink to k3s
[INFO]  Creating /usr/local/bin/crictl symlink to k3s
[INFO]  Skipping /usr/local/bin/ctr symlink to k3s, command exists in PATH at /usr/bin/ctr
[INFO]  Creating killall script /usr/local/bin/k3s-killall.sh
[INFO]  Creating uninstall script /usr/local/bin/k3s-agent-uninstall.sh
[INFO]  env: Creating environment file /etc/systemd/system/k3s-agent.service.env
[INFO]  systemd: Creating service file /etc/systemd/system/k3s-agent.service
[INFO]  systemd: Enabling k3s-agent unit
Created symlink /etc/systemd/system/multi-user.target.wants/k3s-agent.service → /etc/systemd/system/k3s-agent.service.
[INFO]  systemd: Starting k3s-agent
</code></pre></div></div>

<p>Now we finally see some worker nodes added!</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get nodes
NAME                    STATUS   ROLES           AGE   VERSION
builder-macbookpro8-1   Ready    &lt;none&gt;          56s   v1.35.1+k3s1
isaac-macbookair        Ready    control-plane   13h   v1.35.1+k3s1
</code></pre></div></div>

<h1 id="vpa">VPA</h1>

<p>K3s already has a metrics server, so the first thing we really need to do is add the VPA.</p>

<p>The steps I’ll be following are similar to those in <a href="https://thenewstack.io/kubernetes-vpa-inplace-resize/">theNewStack article</a>, but not identical.</p>

<p>We can make it easy by using the autoscaler repo and the install script</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces$ git clone https://github.com/kubernetes/autoscaler.git
Cloning into 'autoscaler'...
remote: Enumerating objects: 239035, done.
remote: Counting objects: 100% (1580/1580), done.
remote: Compressing objects: 100% (1108/1108), done.
remote: Total 239035 (delta 987), reused 475 (delta 471), pack-reused 237455 (from 2)
Receiving objects: 100% (239035/239035), 256.72 MiB | 10.56 MiB/s, done.
Resolving deltas: 100% (155817/155817), done.
Updating files: 100% (5897/5897), done.
builder@DESKTOP-QADGF36:~/Workspaces$ cd autoscaler/
builder@DESKTOP-QADGF36:~/Workspaces/autoscaler$ cd vertical-pod-autoscaler/
builder@DESKTOP-QADGF36:~/Workspaces/autoscaler/vertical-pod-autoscaler$
</code></pre></div></div>

<p>Once cloned down, we can fire up the “hack” install script</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces/autoscaler/vertical-pod-autoscaler$ cat ./hack/vpa-up.sh
#!/bin/bash

# Copyright 2018 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

set -o errexit
set -o nounset
set -o pipefail

SCRIPT_ROOT=$(dirname ${BASH_SOURCE})/..
DEFAULT_TAG="1.6.0"
TAG_TO_APPLY=${TAG-$DEFAULT_TAG}

if [ "${TAG_TO_APPLY}" == "${DEFAULT_TAG}" ]; then
  git switch --detach vertical-pod-autoscaler-${DEFAULT_TAG}
fi

$SCRIPT_ROOT/hack/vpa-process-yamls.sh apply $*
builder@DESKTOP-QADGF36:~/Workspaces/autoscaler/vertical-pod-autoscaler$ ./hack/vpa-up.sh
HEAD is now at 9196162ba Update VPA default version to 1.6.0
customresourcedefinition.apiextensions.k8s.io/verticalpodautoscalercheckpoints.autoscaling.k8s.io created
customresourcedefinition.apiextensions.k8s.io/verticalpodautoscalers.autoscaling.k8s.io created
clusterrole.rbac.authorization.k8s.io/system:metrics-reader created
clusterrole.rbac.authorization.k8s.io/system:vpa-actor created
clusterrole.rbac.authorization.k8s.io/system:vpa-status-actor created
clusterrole.rbac.authorization.k8s.io/system:vpa-checkpoint-actor created
clusterrole.rbac.authorization.k8s.io/system:evictioner created
clusterrole.rbac.authorization.k8s.io/system:vpa-updater-in-place created
clusterrolebinding.rbac.authorization.k8s.io/system:vpa-updater-in-place-binding created
clusterrolebinding.rbac.authorization.k8s.io/system:metrics-reader created
clusterrolebinding.rbac.authorization.k8s.io/system:vpa-actor created
clusterrolebinding.rbac.authorization.k8s.io/system:vpa-status-actor created
clusterrolebinding.rbac.authorization.k8s.io/system:vpa-checkpoint-actor created
clusterrole.rbac.authorization.k8s.io/system:vpa-target-reader created
clusterrolebinding.rbac.authorization.k8s.io/system:vpa-target-reader-binding created
clusterrolebinding.rbac.authorization.k8s.io/system:vpa-evictioner-binding created
serviceaccount/vpa-admission-controller created
serviceaccount/vpa-recommender created
serviceaccount/vpa-updater created
clusterrole.rbac.authorization.k8s.io/system:vpa-admission-controller created
clusterrolebinding.rbac.authorization.k8s.io/system:vpa-admission-controller created
clusterrole.rbac.authorization.k8s.io/system:vpa-status-reader created
clusterrolebinding.rbac.authorization.k8s.io/system:vpa-status-reader-binding created
role.rbac.authorization.k8s.io/system:leader-locking-vpa-updater created
rolebinding.rbac.authorization.k8s.io/system:leader-locking-vpa-updater created
role.rbac.authorization.k8s.io/system:leader-locking-vpa-recommender created
rolebinding.rbac.authorization.k8s.io/system:leader-locking-vpa-recommender created
deployment.apps/vpa-updater created
deployment.apps/vpa-recommender created
Generating certs for the VPA Admission Controller in /tmp/vpa-certs.
Certificate request self-signature ok
subject=CN=vpa-webhook.kube-system.svc
Uploading certs to the cluster.
secret/vpa-tls-certs created
Deleting /tmp/vpa-certs.
service/vpa-webhook created
deployment.apps/vpa-admission-controller created
service/vpa-webhook unchanged
</code></pre></div></div>

<p>With the pods now running</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get po -n kube-system | grep vpa
vpa-admission-controller-5879bdc979-9cf85   1/1     Running   0          2m11s
vpa-recommender-59c7769c67-chrgn            1/1     Running   0          2m12s
vpa-updater-64888c8777-8djfw                1/1     Running   0          2m12s
</code></pre></div></div>

<h2 id="setup">Setup</h2>

<p>First, I need some basic app for which to test.</p>

<p>Let’s fire up a simple hello-app</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl create deployment hello-server --image=us-docker.pkg.dev/google-samples/containers/gke/hello-app:1.0

$ kubectl get po
NAME                            READY   STATUS    RESTARTS   AGE
hello-server-66bd54ff48-vmjsx   1/1     Running   0          32s
</code></pre></div></div>

<p>I’ll do a nodeport service so we can see it without having to port-forward</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl expose deployment hello-server --type=NodePort --name=hello-service --port=8080
service/hello-service exposed

$ kubectl get svc
NAME            TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
hello-service   NodePort    10.43.50.228   &lt;none&gt;        8080:31445/TCP   2m56s
kubernetes      ClusterIP   10.43.0.1      &lt;none&gt;        443/TCP          12h
</code></pre></div></div>

<p>And here is our app</p>

<p><a href="/content/images/2026/03/k3supgrade-04.png"><img src="/content/images/2026/03/k3supgrade-04.png" alt="/content/images/2026/03/k3supgrade-04.png" /></a></p>

<h2 id="updates-without-a-vpa">Updates without a VPA</h2>

<p>Without a VPA, what happens when we edit our deployment to add some resource settings (or change them).</p>

<p>For instance, adding</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="na">resources</span><span class="pi">:</span>
    <span class="na">requests</span><span class="pi">:</span>
        <span class="na">cpu</span><span class="pi">:</span> <span class="s2">"</span><span class="s">50m"</span>
        <span class="na">memory</span><span class="pi">:</span> <span class="s2">"</span><span class="s">64Mi"</span>
    <span class="na">limits</span><span class="pi">:</span>
        <span class="na">cpu</span><span class="pi">:</span> <span class="s2">"</span><span class="s">200m"</span>
        <span class="na">memory</span><span class="pi">:</span> <span class="s2">"</span><span class="s">256Mi"</span>
</code></pre></div></div>

<p>rotates the pods:</p>

<video muted="" controls="">
    <source src="/content/images/2026/03/vpa-01.mp4" type="video/mp4" />
</video>

<p>This means that if you have a mission critical workload that could take downtime rotating pods, changing your settings and limits is something pushed to the off-hours or emergency code red times.</p>

<h2 id="updates-with-a-vpa">Updates with a VPA</h2>

<p>However, with VPAs, the Vertical Pod Autoscaler can work with the Metrics server to monitor our workload and adjust settings on demand - without modifying any running pod</p>

<p>Let’s create a new VPA for our deployment, but just to test things, we have “updateMode” set to “off” so it’s going to be a recommendation agent for now.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: hello-vpa
spec:
  targetRef:
    apiVersion: "apps/v1"
    kind: Deployment
    name: hello-server
  updatePolicy:
    updateMode: "Off"
  resourcePolicy:
    containerPolicies:
    - containerName: "hello-app"
      minAllowed:
        cpu: "25m"
        memory: "32Mi"
      maxAllowed:
        cpu: "1"
        memory: "512Mi"
      controlledResources: ["cpu", "memory"]
</code></pre></div></div>

<p>We’ll apply</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl apply -f ./vpa.yaml
verticalpodautoscaler.autoscaling.k8s.io/hello-vpa created
</code></pre></div></div>

<p>While you <em>can</em> use the YAML output for verbose details</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get vpa hello-vpa -o yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"autoscaling.k8s.io/v1","kind":"VerticalPodAutoscaler","metadata":{"annotations":{},"name":"hello-vpa","namespace":"default"},"spec":{"resourcePolicy":{"containerPolicies":[{"containerName":"hello-app","controlledResources":["cpu","memory"],"maxAllowed":{"cpu":"1","memory":"512Mi"},"minAllowed":{"cpu":"25m","memory":"32Mi"}}]},"targetRef":{"apiVersion":"apps/v1","kind":"Deployment","name":"hello-server"},"updatePolicy":{"updateMode":"Off"}}}
  creationTimestamp: "2026-03-18T13:47:23Z"
  generation: 1
  name: hello-vpa
  namespace: default
  resourceVersion: "25299"
  uid: dce05dec-2e90-4eb3-ad2f-5daf5a1d489b
spec:
  resourcePolicy:
    containerPolicies:
    - containerName: hello-app
      controlledResources:
      - cpu
      - memory
      maxAllowed:
        cpu: "1"
        memory: 512Mi
      minAllowed:
        cpu: 25m
        memory: 32Mi
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: hello-server
  updatePolicy:
    updateMode: "Off"
status:
  conditions:
  - lastTransitionTime: "2026-03-18T13:47:35Z"
    status: "True"
    type: RecommendationProvided
  recommendation:
    containerRecommendations:
    - containerName: hello-app
      lowerBound:
        cpu: 25m
        memory: 250Mi
      target:
        cpu: 25m
        memory: 250Mi
      uncappedTarget:
        cpu: 25m
        memory: 250Mi
      upperBound:
        cpu: 213m
        memory: 250Mi
</code></pre></div></div>

<p>I rather prefer the wide output:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get vpa hello-vpa -o wide
NAME        MODE   CPU   MEM     PROVIDED   AGE
hello-vpa   Off    25m   250Mi   True       96s
</code></pre></div></div>

<p>next, I want to allow this to actually modify the resources by changing our update to “InPlaceOrRecreate”.  This means it will first try to “patch” the pod, but if the pod acts like a little turd, it will replace and evict it next.</p>

<p>You’ll notice I change the updateMode as well as add “controlledValues” at the end:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cat vpa.yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metad
  name: hello-vpa
spec:
  targetRef:
    apiVersion: "apps/v1"
    kind: Deployment
    name: hello-server
  updatePolicy:
    updateMode: "InPlaceOrRecreate"
  resourcePolicy:
    containerPolicies:
    - containerName: "hello-app"
      minAllowed:
        cpu: "25m"
        memory: "32Mi"
      maxAllowed:
        cpu: "1"
        memory: "512Mi"
      controlledResources: ["cpu", "memory"]
      controlledValues: "RequestsAndLimits"
$ kubectl apply -f ./vpa.yaml
verticalpodautoscaler.autoscaling.k8s.io/hello-vpa configured
$ kubectl get vpa hello-vpa -o wide
NAME        MODE                CPU   MEM     PROVIDED   AGE
hello-vpa   InPlaceOrRecreate   25m   250Mi   True       5m4s
</code></pre></div></div>

<p>I can see the pod was not cycled</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get po
NAME                           READY   STATUS    RESTARTS   AGE
hello-server-cf5d4d7d5-fddxs   1/1     Running   0          78m
</code></pre></div></div>

<p>But I also don’t see it modified</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl describe po hello-server-cf5d4d7d5-fddxs | tail -n 30 | head -n 10
    Ready:          True
    Restart Count:  0
    Limits:
      cpu:     200m
      memory:  256Mi
    Requests:
      cpu:        50m
      memory:     64Mi
    Environment:  &lt;none&gt;
    Mounts:
</code></pre></div></div>

<p>Let’s create some load and see what happens</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl apply -f ./beat-it-up.yaml
pod/load-generator-1 created
pod/load-generator-2 created

$ kubectl get po
NAME                           READY   STATUS    RESTARTS   AGE
hello-server-cf5d4d7d5-fddxs   1/1     Running   0          89m
load-generator-1               1/1     Running   0          101s
load-generator-2               1/1     Running   0          101s
</code></pre></div></div>

<p>I let it run for a bit then checked the recommendations</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get vpa hello-vpa -o jsonpath='{.status.recommendation.containerRecommendations[0].target}' ; echo
{"cpu":"25m","memory":"250Mi"}
</code></pre></div></div>

<p>The pod was still in place</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get po
NAME                           READY   STATUS    RESTARTS   AGE
hello-server-cf5d4d7d5-fddxs   1/1     Running   0          95m
load-generator-1               1/1     Running   0          7m12s
load-generator-2               1/1     Running   0          7m12s
</code></pre></div></div>

<p>with the same resource limits/requests</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl describe po hello-server-cf5d4d7d5-fddxs | tail -n 30 | head -n 10
    Ready:          True
    Restart Count:  0
    Limits:
      cpu:     200m
      memory:  256Mi
    Requests:
      cpu:        50m
      memory:     64Mi
    Environment:  &lt;none&gt;
    Mounts:
</code></pre></div></div>

<p>I bumped my load generators up to 4 just really apply some pressure and then saw the numbers climb</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get vpa hello-vpa -o jsonpath='{.status.recommendation.containerRecommendations[0].target}' ; echo
{"cpu":"247m","memory":"250Mi"}
</code></pre></div></div>

<p>The thing is the deployment has yet to change and the pod is still using the smaller requests</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl describe po -l app=hello-server | grep -A 3 "Requests:"
    Requests:
      cpu:        50m
      memory:     64Mi
    Environment:  &lt;none&gt;
</code></pre></div></div>

<p>the VPA is clearly showing it should replace</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get vpa hello-vpa
NAME        MODE                CPU    MEM     PROVIDED   AGE
hello-vpa   InPlaceOrRecreate   247m   250Mi   True       39m
</code></pre></div></div>

<p>I tried recreating the VPA as well with the harsher “Recreate” option</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cat vpa.yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: hello-vpa
spec:
  targetRef:
    apiVersion: "apps/v1"
    kind: Deployment
    name: hello-server
  updatePolicy:
    updateMode: "Recreate"
  resourcePolicy:
    containerPolicies:
    - containerName: "hello-app"
      minAllowed:
        cpu: "25m"
        memory: "32Mi"
      maxAllowed:
        cpu: "1"
        memory: "512Mi"
      controlledResources: ["cpu", "memory"]
      controlledValues: "RequestsAndLimits"
$ kubectl apply -f ./vpa.yaml
verticalpodautoscaler.autoscaling.k8s.io/hello-vpa configured
</code></pre></div></div>

<p>That seemed to do nothing</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get vpa hello-vpa
NAME        MODE       CPU    MEM     PROVIDED   AGE
hello-vpa   Recreate   247m   250Mi   True       41m
$ kubectl describe po -l app=hello-server | grep -A 3 "Requests:"
    Requests:
      cpu:        50m
      memory:     64Mi
    Environment:  &lt;none&gt;
$ kubectl get po
NAME                           READY   STATUS    RESTARTS   AGE
hello-server-cf5d4d7d5-fddxs   1/1     Running   0          113m
load-generator-1               1/1     Running   0          12m
load-generator-2               1/1     Running   0          12m
load-generator-3               1/1     Running   0          12m
load-generator-4               1/1     Running   0          12m
</code></pre></div></div>

<p>I then forced the VPA to recreate</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl delete vpa hello-vpa
verticalpodautoscaler.autoscaling.k8s.io "hello-vpa" deleted
$ kubectl apply -f ./vpa.yaml
verticalpodautoscaler.autoscaling.k8s.io/hello-vpa created
$ kubectl get vpa hello-vpa
NAME        MODE       CPU   MEM   PROVIDED   AGE
hello-vpa   Recreate                          8s
</code></pre></div></div>

<p>It took about 40s to show fresh recommendations</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get vpa hello-vpa
NAME        MODE       CPU   MEM   PROVIDED   AGE
hello-vpa   Recreate                          6s

$ kubectl get vpa hello-vpa
NAME        MODE       CPU    MEM     PROVIDED   AGE
hello-vpa   Recreate   247m   250Mi   True       49s

</code></pre></div></div>

<p>But nothing changed</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get po
NAME                           READY   STATUS    RESTARTS   AGE
hello-server-cf5d4d7d5-fddxs   1/1     Running   0          150m
load-generator-1               1/1     Running   0          49m
load-generator-2               1/1     Running   0          49m
load-generator-3               1/1     Running   0          49m
load-generator-4               1/1     Running   0          49m
</code></pre></div></div>

<h2 id="pivot">Pivot</h2>

<p>Let’s try using the Nginx pod that the original article used instead.  I want to see if it’s just the lightweight app I was using</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl create ns vpa-demo
namespace/vpa-demo created

$ cat ./vpa-nginx.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-vpa-demo
  namespace: vpa-demo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx-vpa-demo
  template:
    metadata:
      labels:
        app: nginx-vpa-demo
    spec:
      containers:
      - name: nginx
        image: nginx:1.25
        resources:
          requests:
            cpu: "50m"
            memory: "64Mi"
          limits:
            cpu: "200m"
            memory: "256Mi"
</code></pre></div></div>

<p>then apply</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl apply -f ./vpa-nginx.yaml
deployment.apps/nginx-vpa-demo created

</code></pre></div></div>

<p>I can see the pods are up</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get po -n vpa-demo -l app=nginx-vpa-demo
NAME                              READY   STATUS    RESTARTS   AGE
nginx-vpa-demo-58d8649cf4-d85cb   1/1     Running   0          8m53s
nginx-vpa-demo-58d8649cf4-ptp9c   1/1     Running   0          8m53s
</code></pre></div></div>

<p>let’s do the recommendation version</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cat vpa_nginx.yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: nginx-vpa
  namespace: vpa-demo
spec:
  targetRef:
    apiVersion: "apps/v1"
    kind: Deployment
    name: nginx-vpa-demo
  updatePolicy:
    updateMode: "Off"
  resourcePolicy:
    containerPolicies:
    - containerName: "nginx"
      minAllowed:
        cpu: "25m"
        memory: "32Mi"
      maxAllowed:
        cpu: "1"
        memory: "512Mi"
      controlledResources: ["cpu", "memory"]
$ kubectl apply -f ./vpa_nginx.yaml
verticalpodautoscaler.autoscaling.k8s.io/nginx-vpa created
</code></pre></div></div>

<p>And see it works</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get vpa -n vpa-demo
NAME        MODE   CPU   MEM     PROVIDED   AGE
nginx-vpa   Off    25m   250Mi   True       60s
</code></pre></div></div>

<p>I’ll now flip it to inplace replacements</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cat vpa-inplace.yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: nginx-vpa
  namespace: vpa-demo
spec:
  targetRef:
    apiVersion: "apps/v1"
    kind: Deployment
    name: nginx-vpa-demo
  updatePolicy:
    updateMode: "InPlaceOrRecreate"
  resourcePolicy:
    containerPolicies:
    - containerName: "nginx"
      minAllowed:
        cpu: "25m"
        memory: "32Mi"
      maxAllowed:
        cpu: "1"
        memory: "512Mi"
      controlledResources: ["cpu", "memory"]
      controlledValues: "RequestsAndLimits"

$ kubectl apply -f ./vpa-inplace.yaml
verticalpodautoscaler.autoscaling.k8s.io/nginx-vpa configured

$ kubectl get vpa -n vpa-demo
NAME        MODE                CPU   MEM     PROVIDED   AGE
nginx-vpa   InPlaceOrRecreate   25m   250Mi   True       3m33s
</code></pre></div></div>

<p>I see the requests</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl describe pod -n vpa-demo -l app=nginx-vpa-demo | grep -A 3 "Requests:"
    Requests:
      cpu:        25m
      memory:     250Mi
    Environment:  &lt;none&gt;
--
    Requests:
      cpu:        25m
      memory:     250Mi
    Environment:  &lt;none&gt;
</code></pre></div></div>

<p>The article was just using pods, but let’s turn it to 11 and just make the load tester a deployment</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">$ cat ./beat-it-up-new.yaml</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">nginx-vpa-demo</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">vpa-demo</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">nginx-vpa-demo</span>
  <span class="na">ports</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">port</span><span class="pi">:</span> <span class="m">80</span>
    <span class="na">targetPort</span><span class="pi">:</span> <span class="m">80</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">load-generator</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">vpa-demo</span>
  <span class="na">labels</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">load-generator</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">10</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">load-generator</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app</span><span class="pi">:</span> <span class="s">load-generator</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">busybox</span>
        <span class="na">image</span><span class="pi">:</span> <span class="s">busybox:1.36</span>
        <span class="na">command</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="s">/bin/sh</span>
        <span class="pi">-</span> <span class="s">-c</span>
        <span class="pi">-</span> <span class="pi">|</span>
          <span class="s">echo "Starting load generator..."</span>
          <span class="s">while true; do</span>
            <span class="s">wget -q -O- http://nginx-vpa-demo.vpa-demo.svc.cluster.local &gt; /dev/null 2&gt;&amp;1</span>
          <span class="s">done</span>
      <span class="na">restartPolicy</span><span class="pi">:</span> <span class="s">Always</span>


<span class="s">$ kubectl apply -f ./beat-it-up-new.yaml</span>
<span class="s">service/nginx-vpa-demo created</span>
<span class="s">deployment.apps/load-generator created</span>


</code></pre></div></div>

<p>I can now see pods fired up</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get po -n vpa-demo
NAME                              READY   STATUS    RESTARTS   AGE
load-generator-b5756b46-7gbz8     1/1     Running   0          36s
load-generator-b5756b46-8nvcq     1/1     Running   0          36s
load-generator-b5756b46-c6lkv     1/1     Running   0          36s
load-generator-b5756b46-f4txq     1/1     Running   0          36s
load-generator-b5756b46-gzd4z     1/1     Running   0          36s
load-generator-b5756b46-n6lkt     1/1     Running   0          36s
load-generator-b5756b46-p82cl     1/1     Running   0          36s
load-generator-b5756b46-qznc9     1/1     Running   0          36s
load-generator-b5756b46-rwsdz     1/1     Running   0          36s
load-generator-b5756b46-z6czb     1/1     Running   0          36s
nginx-vpa-demo-58d8649cf4-d85cb   1/1     Running   0          20m
nginx-vpa-demo-58d8649cf4-ptp9c   1/1     Running   0          20m
</code></pre></div></div>

<p>I did a follow to see indeed the Nginx pods were getting hit heavy</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl logs nginx-vpa-demo-58d8649cf4-d85cb -n vpa-demo --follow
</code></pre></div></div>

<p>After a bit I saw the recommendations go up</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get vpa nginx-vpa -n vpa-demo -o jsonpath='{.status.recommendation.containerRecommendations[0].target}' ; echo
{"cpu":"25m","memory":"250Mi"}

$ kubectl get vpa nginx-vpa -n vpa-demo -o jsonpath='{.status.recommendation.containerRecommendations[0].target}' ; echo
{"cpu":"93m","memory":"250Mi"}
</code></pre></div></div>

<p>But no pod updates yet</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl describe pod -n vpa-demo -l app=nginx-vpa-demo | grep -A 3 "Requests:"
    Requests:
      cpu:        25m
      memory:     250Mi
    Environment:  &lt;none&gt;
--
    Requests:
      cpu:        25m
      memory:     250Mi
    Environment:  &lt;none&gt;
</code></pre></div></div>

<p>Let’s really set the burn</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get deployment -n vpa-demo
NAME             READY   UP-TO-DATE   AVAILABLE   AGE
load-generator   10/10   10           10          15m
nginx-vpa-demo   2/2     2            2           35m

$ kubectl scale deployment -n vpa-demo load-generator --replicas=100
deployment.apps/load-generator scaled

$ kubectl get deployment -n vpa-demo
NAME             READY    UP-TO-DATE   AVAILABLE   AGE
load-generator   10/100   100          10          15m
nginx-vpa-demo   2/2      2            2           36m
</code></pre></div></div>

<p>Soon the pods were slammed</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get deployment -n vpa-demo
NAME             READY     UP-TO-DATE   AVAILABLE   AGE
load-generator   100/100   100          100         17m
nginx-vpa-demo   2/2       2            2           37m
</code></pre></div></div>

<p>But not rotated</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get po  -n vpa-demo -l app=nginx-vpa-demo
NAME                              READY   STATUS    RESTARTS   AGE
nginx-vpa-demo-58d8649cf4-d85cb   1/1     Running   0          38m
nginx-vpa-demo-58d8649cf4-ptp9c   1/1     Running   0          38m
</code></pre></div></div>

<h2 id="success">Success!!!</h2>

<p>However, on looking at the Events, I finally saw what I was hoping for - resize events:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl describe po nginx-vpa-demo-58d8649cf4-d85cb -n vpa-demo
Name:             nginx-vpa-demo-58d8649cf4-d85cb
Namespace:        vpa-demo
Priority:         0
Service Account:  default
Node:             builder-macbookpro8-1/192.168.1.205
Start Time:       Wed, 18 Mar 2026 10:08:19 -0500
Labels:           app=nginx-vpa-demo
                  pod-template-hash=58d8649cf4
Annotations:      vpaInPlaceUpdated: true
Status:           Running
IP:               10.42.1.10
IPs:
  IP:           10.42.1.10
Controlled By:  ReplicaSet/nginx-vpa-demo-58d8649cf4
Containers:
  nginx:
    Container ID:   containerd://0eeab3e68bcc2a8c9e8341851c7216d8f9f54287cc5ce636e0945fb8074494ea
    Image:          nginx:1.25
    Image ID:       docker.io/library/nginx@sha256:a484819eb60211f5299034ac80f6a681b06f89e65866ce91f356ed7c72af059c
    Port:           &lt;none&gt;
    Host Port:      &lt;none&gt;
    State:          Running
      Started:      Wed, 18 Mar 2026 10:08:41 -0500
    Ready:          True
    Restart Count:  0
    Limits:
      cpu:     100m
      memory:  1000Mi
    Requests:
      cpu:        25m
      memory:     250Mi
    Environment:  &lt;none&gt;
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-2m45t (ro)
Conditions:
  Type                        Status
  PodReadyToStartContainers   True
  Initialized                 True
  Ready                       True
  ContainersReady             True
  PodScheduled                True
Volumes:
  kube-api-access-2m45t:
    Type:                    Projected (a volume that contains injected data from multiple sources)
    TokenExpirationSeconds:  3607
    ConfigMapName:           kube-root-ca.crt
    Optional:                false
    DownwardAPI:             true
QoS Class:                   Burstable
Node-Selectors:              &lt;none&gt;
Tolerations:                 node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
                             node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:
  Type    Reason               Age   From               Message
  ----    ------               ----  ----               -------
  Normal  Scheduled            38m   default-scheduler  Successfully assigned vpa-demo/nginx-vpa-demo-58d8649cf4-d85cb to builder-macbookpro8-1
  Normal  Pulling              38m   kubelet            Pulling image "nginx:1.25"
  Normal  Pulled               38m   kubelet            Successfully pulled image "nginx:1.25" in 18.601s (18.601s including waiting). Image size: 71005258 bytes.
  Normal  Created              38m   kubelet            Container created
  Normal  Started              38m   kubelet            Container started
  Normal  InPlaceResizedByVPA  24m   vpa-updater        Pod was resized in place by VPA Updater.
  Normal  ResizeStarted        24m   kubelet            Pod resize started: {"containers":[{"name":"nginx","resources":{"limits":{"cpu":"100m","memory":"1000Mi"},"requests":{"cpu":"25m","memory":"250Mi"}}}],"generation":2}
  Normal  ResizeCompleted      24m   kubelet            Pod resize completed: {"containers":[{"name":"nginx","resources":{"limits":{"cpu":"100m","memory":"1000Mi"},"requests":{"cpu":"25m","memory":"250Mi"}}}],"generation":2}
</code></pre></div></div>

<h2 id="the-disconnect">The disconnect</h2>

<p>The <a href="https://thenewstack.io/kubernetes-vpa-inplace-resize/">original article</a> had suggested we might see the requests change</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl describe pod -n vpa-demo -l app=nginx-vpa-demo | grep -A 3 "Requests:"
    Requests:
      cpu:        25m
      memory:     250Mi
    Environment:  &lt;none&gt;
--
    Requests:
      cpu:        25m
      memory:     250Mi
    Environment:  &lt;none&gt;
</code></pre></div></div>

<p>But think about it - the <em>requests</em> really are fine.. its the limits that need fixing!</p>

<p>If you look up above, you’ll note we set those limits 200 milicore (0.2 CPU) and 256MiB (memory):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    requests:
        cpu: "50m"
        memory: "64Mi"
    limits:
        cpu: "200m"
        memory: "256Mi"
</code></pre></div></div>

<p>however, our VPA has moved those upper bounds for us, actually lowering the CPU but greatly increasing the memory</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl describe pod -n vpa-demo -l app=nginx-vpa-demo | grep -A 3 "Limits:"
    Limits:
      cpu:     100m
      memory:  1000Mi
    Requests:
--
    Limits:
      cpu:     100m
      memory:  1000Mi
    Requests:
</code></pre></div></div>

<p>Let’s remove the pressure</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl delete -f ./beat-it-up-new.yaml
service "nginx-vpa-demo" deleted
deployment.apps "load-generator" deleted

$ kubectl get po -n vpa-demo
NAME                              READY   STATUS        RESTARTS   AGE
load-generator-b5756b46-2bccv     1/1     Terminating   0          8m41s
load-generator-b5756b46-2lwr7     1/1     Terminating   0          8m38s
load-generator-b5756b46-2ml7z     1/1     Terminating   0          8m38s
load-generator-b5756b46-2pv22     1/1     Terminating   0          8m37s
load-generator-b5756b46-487nl     1/1     Terminating   0          8m41s
load-generator-b5756b46-4nplc     1/1     Terminating   0          8m40s
load-generator-b5756b46-4t59c     1/1     Terminating   0          8m41s
load-generator-b5756b46-4wkgz     1/1     Terminating   0          8m42s
load-generator-b5756b46-562wc     1/1     Terminating   0          8m37s
load-generator-b5756b46-56z92     1/1     Terminating   0          8m39s
load-generator-b5756b46-5946h     1/1     Terminating   0          8m38s
load-generator-b5756b46-5l6wj     1/1     Terminating   0          8m41s
load-generator-b5756b46-5sjrq     1/1     Terminating   0          8m39s
load-generator-b5756b46-5xc47     1/1     Terminating   0          8m36s
load-generator-b5756b46-6749w     1/1     Terminating   0          8m40s
load-generator-b5756b46-68h87     1/1     Terminating   0          8m39s
load-generator-b5756b46-6jds9     1/1     Terminating   0          8m38s
load-generator-b5756b46-6x2nb     1/1     Terminating   0          8m40s
load-generator-b5756b46-787lb     1/1     Terminating   0          8m38s
load-generator-b5756b46-7gbz8     1/1     Terminating   0          24m
load-generator-b5756b46-7j8wp     1/1     Terminating   0          8m41s
load-generator-b5756b46-7lq55     1/1     Terminating   0          8m41s
load-generator-b5756b46-7twkb     1/1     Terminating   0          8m37s
load-generator-b5756b46-8nvcq     1/1     Terminating   0          24m
load-generator-b5756b46-8qjc5     1/1     Terminating   0          8m42s
load-generator-b5756b46-8sxvl     1/1     Terminating   0          8m41s
load-generator-b5756b46-8tjt6     1/1     Terminating   0          8m38s
load-generator-b5756b46-8vjlp     1/1     Terminating   0          8m39s
load-generator-b5756b46-8x7z8     1/1     Terminating   0          8m36s
load-generator-b5756b46-bkzf9     1/1     Terminating   0          8m36s
load-generator-b5756b46-bmgbt     1/1     Terminating   0          8m41s
load-generator-b5756b46-bs6pq     1/1     Terminating   0          8m39s
load-generator-b5756b46-c6cwp     1/1     Terminating   0          8m41s
load-generator-b5756b46-c6lkv     1/1     Terminating   0          24m
load-generator-b5756b46-c7n9j     1/1     Terminating   0          8m37s
load-generator-b5756b46-chtm6     1/1     Terminating   0          8m38s
load-generator-b5756b46-dh5gj     1/1     Terminating   0          8m41s
load-generator-b5756b46-f4s26     1/1     Terminating   0          8m41s
load-generator-b5756b46-f4txq     1/1     Terminating   0          24m
load-generator-b5756b46-f8whk     1/1     Terminating   0          8m42s
load-generator-b5756b46-fqnxl     1/1     Terminating   0          8m42s
load-generator-b5756b46-ft9sn     1/1     Terminating   0          8m41s
load-generator-b5756b46-g2cd9     1/1     Terminating   0          8m40s
load-generator-b5756b46-g8x6r     1/1     Terminating   0          8m41s
load-generator-b5756b46-gm7hj     1/1     Terminating   0          8m41s
load-generator-b5756b46-gt9md     1/1     Terminating   0          8m40s
load-generator-b5756b46-gxqtw     1/1     Terminating   0          8m40s
load-generator-b5756b46-gzd4z     1/1     Terminating   0          24m
load-generator-b5756b46-gznnz     1/1     Terminating   0          8m41s
load-generator-b5756b46-h6qh4     1/1     Terminating   0          8m40s
load-generator-b5756b46-j8hvz     1/1     Terminating   0          8m41s
load-generator-b5756b46-jhzvp     1/1     Terminating   0          8m41s
load-generator-b5756b46-jprd8     1/1     Terminating   0          8m38s
load-generator-b5756b46-jsb6j     1/1     Terminating   0          8m37s
load-generator-b5756b46-k4hk8     1/1     Terminating   0          8m41s
load-generator-b5756b46-khjns     1/1     Terminating   0          8m37s
load-generator-b5756b46-knwmf     1/1     Terminating   0          8m38s
load-generator-b5756b46-knwsw     1/1     Terminating   0          8m37s
load-generator-b5756b46-lcm88     1/1     Terminating   0          8m42s
load-generator-b5756b46-ldf9b     1/1     Terminating   0          8m41s
load-generator-b5756b46-lxcqj     1/1     Terminating   0          8m41s
load-generator-b5756b46-m67nt     1/1     Terminating   0          8m40s
load-generator-b5756b46-mn6md     1/1     Terminating   0          8m41s
load-generator-b5756b46-n6lkt     1/1     Terminating   0          24m
load-generator-b5756b46-n8psf     1/1     Terminating   0          8m40s
load-generator-b5756b46-n9dsk     1/1     Terminating   0          8m40s
load-generator-b5756b46-nb4cl     1/1     Terminating   0          8m37s
load-generator-b5756b46-nbqnh     1/1     Terminating   0          8m42s
load-generator-b5756b46-nbwxf     1/1     Terminating   0          8m38s
load-generator-b5756b46-nt4n5     1/1     Terminating   0          8m41s
load-generator-b5756b46-p67nk     1/1     Terminating   0          8m39s
load-generator-b5756b46-p82cl     1/1     Terminating   0          24m
load-generator-b5756b46-pk22x     1/1     Terminating   0          8m39s
load-generator-b5756b46-ptqdh     1/1     Terminating   0          8m41s
load-generator-b5756b46-pvhqn     1/1     Terminating   0          8m41s
load-generator-b5756b46-q6dp8     1/1     Terminating   0          8m41s
load-generator-b5756b46-qcswz     1/1     Terminating   0          8m41s
load-generator-b5756b46-qkmtj     1/1     Terminating   0          8m40s
load-generator-b5756b46-qnsmh     1/1     Terminating   0          8m40s
load-generator-b5756b46-qt65f     1/1     Terminating   0          8m38s
load-generator-b5756b46-qznc9     1/1     Terminating   0          24m
load-generator-b5756b46-rmzm5     1/1     Terminating   0          8m40s
load-generator-b5756b46-rrm4g     1/1     Terminating   0          8m41s
load-generator-b5756b46-rwsdz     1/1     Terminating   0          24m
load-generator-b5756b46-sb7zm     1/1     Terminating   0          8m41s
load-generator-b5756b46-snmww     1/1     Terminating   0          8m40s
load-generator-b5756b46-t2jtj     1/1     Terminating   0          8m42s
load-generator-b5756b46-t455g     1/1     Terminating   0          8m38s
load-generator-b5756b46-tjgjb     1/1     Terminating   0          8m38s
load-generator-b5756b46-tpqx8     1/1     Terminating   0          8m40s
load-generator-b5756b46-vnf4t     1/1     Terminating   0          8m41s
load-generator-b5756b46-w4k42     1/1     Terminating   0          8m41s
load-generator-b5756b46-wg96d     1/1     Terminating   0          8m40s
load-generator-b5756b46-whmfq     1/1     Terminating   0          8m40s
load-generator-b5756b46-wwvgk     1/1     Terminating   0          8m42s
load-generator-b5756b46-xldl6     1/1     Terminating   0          8m41s
load-generator-b5756b46-xm885     1/1     Terminating   0          8m36s
load-generator-b5756b46-z6czb     1/1     Terminating   0          24m
load-generator-b5756b46-z7crv     1/1     Terminating   0          8m37s
load-generator-b5756b46-zl27d     1/1     Terminating   0          8m36s
nginx-vpa-demo-58d8649cf4-d85cb   1/1     Running       0          44m
nginx-vpa-demo-58d8649cf4-ptp9c   1/1     Running       0          44m
</code></pre></div></div>

<p>I should soon see the numbers on the recommendation fall</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get vpa nginx-vpa -n vpa-demo -o jsonpath='{.status.recommendation.containerRecommendations[0].target}' ; echo
{"cpu":"109m","memory":"250Mi"}

$ kubectl describe pod -n vpa-demo -l app=nginx-vpa-demo | grep -A 3 "Limits:"
    Limits:
      cpu:     100m
      memory:  1000Mi
    Requests:
--
    Limits:
      cpu:     100m
      memory:  1000Mi
    Requests:
</code></pre></div></div>

<p>However, even after a good hour, I didn’t see it really scale back down</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get vpa -n vpa-demo
NAME        MODE                CPU   MEM     PROVIDED   AGE
nginx-vpa   InPlaceOrRecreate   78m   250Mi   True       131m

$ kubectl get vpa nginx-vpa -n vpa-demo -o jsonpath='{.status.recommendation.containerRecommendations[0].target}' ; echo
{"cpu":"78m","memory":"250Mi"}

$ kubectl describe pod -n vpa-demo -l app=nginx-vpa-demo | grep -A 3 "Limits:"
    Limits:
      cpu:     100m
      memory:  1000Mi
    Requests:
--
    Limits:
      cpu:     100m
      memory:  1000Mi
    Requests:
$ kubectl describe pod -n vpa-demo -l app=nginx-vpa-demo | grep -A 3 "Requests:"
    Requests:
      cpu:        25m
      memory:     250Mi
    Environment:  &lt;none&gt;
--
    Requests:
      cpu:        25m
      memory:     250Mi
    Environment:  &lt;none&gt;
</code></pre></div></div>

<p>Perhaps it’s happy…  I noticed the events got wiped over time so really I would need to add a proper Prometheus stack or observability engine to see it over a longer period.</p>

<h1 id="summary">Summary</h1>

<p>Well, this took a lot longer than I had hoped and in the end, caused me to rebuild two old laptops, but we did test VPAs in Kubernetes 1.35.  Most of the details of this are in the <a href="https://kubernetes.io/docs/concepts/workloads/autoscaling/vertical-pod-autoscale/">official Vertical Pod Autoscaling docs</a> which closely match what we covered.</p>

<p>We covered the “Recreate” (though it didn’t do it), “InPlaceOrRecreate” (do it or i boot you) and “Off” update modes.  There is a fourth mode, “Initial”, which we didn’t cover, but is worth mentioning.  In “Initial”, it sets the requests <em>just</em> when the pods are first created and doesn’t touch them after.  This can be useful for gradual horizontal scale outs or changes that roll slowly over time.</p>]]></content><author><name>Isaac Johnson</name></author><category term="VPA" /><category term="K3s" /><category term="Opensource" /><category term="containers" /><category term="kubernetes" /><summary type="html"><![CDATA[I had bookmarked this article on NewStack about VerticalPodAutoscaling finally going GA. It’s been around for a while, and can be really useful with StatefulSets. I wanted to try in on Deployments (Replicasets) and see how it really works.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://freshbrewed.science/content/images/2026/03/k3supgrade-05.png" /><media:content medium="image" url="https://freshbrewed.science/content/images/2026/03/k3supgrade-05.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Chats: IRC and Matrix</title><link href="https://freshbrewed.science/2026/03/12/ircmatrix.html" rel="alternate" type="text/html" title="Chats: IRC and Matrix" /><published>2026-03-12T01:01:01+00:00</published><updated>2026-03-12T01:01:01+00:00</updated><id>https://freshbrewed.science/2026/03/12/ircmatrix</id><content type="html" xml:base="https://freshbrewed.science/2026/03/12/ircmatrix.html"><![CDATA[<p>Let’s talk about Chat clients.  I got to wondering what every happened to IRC.  Internet Relay Chat (IRC) has been around since 1988, but I certainly got to know it later in 1996.  It was my usual hangout space in the early 2000s but maybe I just got busy - I stopped using it in the ’10s.  It never went away.  It hasn’t changed at all and only recently, after about 22 years did people hop off Freenode to Libera.chat as the primary server.</p>

<p>We have Matrix as well  (I’ve had a Matrix Synapse server here since 2024) but it really didn’t take off.  Is IRC any good?  With all these negative changes happening to the formerly good and reliable tools like Slack and Discord, is it time to go back to the tried and true? Is it time to <a href="https://www.youtube.com/watch?v=ulJBJYie0l8">Bring back the Time?</a></p>

<h1 id="inspirecd-dependencies">Inspirecd Dependencies</h1>

<p>Let’s make sure we have the build essential pieces we need</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ sudo apt update
$ sudo apt install -y build-essential git perl g++ make libssl-dev pkg-config
</code></pre></div></div>

<p>Next, we can clone down Inspirecd from Github</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@bosgamerz9:~$ cd Workspaces/
builder@bosgamerz9:~/Workspaces$ git clone https://github.com/inspircd/inspircd.git
Cloning into 'inspircd'...
remote: Enumerating objects: 159000, done.
remote: Counting objects: 100% (1145/1145), done.
remote: Compressing objects: 100% (455/455), done.
remote: Total 159000 (delta 899), reused 801 (delta 689), pack-reused 157855 (from 3)
Receiving objects: 100% (159000/159000), 43.06 MiB | 849.00 KiB/s, done.
Resolving deltas: 100% (133861/133861), done.
</code></pre></div></div>

<p>I went to configure</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@bosgamerz9:~/Workspaces/inspircd$ perl ./configure --enable-extras ssl_openssl
Enabling the ssl_openssl module ...
argon2          = disabled
geo_maxmind     = disabled
ldap            = disabled
log_json        = disabled
log_syslog      = disabled
mysql           = disabled
pgsql           = disabled
regex_pcre2     = disabled
regex_posix     = disabled
regex_re2       = disabled
regex_tre       = disabled
sqlite3         = disabled
ssl_gnutls      = disabled
ssl_openssl     = enabled
sslrehashsignal = disabled
Remember: YOU are responsible for making sure any libraries needed have been installed!


builder@bosgamerz9:~/Workspaces/inspircd$ perl ./configure
Configuring InspIRCd 4.9.0-cab23b7e5 on Linux 6.14.0-29-generic x86_64.
Checking whether .configure/cache.cfg is available ... no
Checking whether /home/builder/Workspaces/inspircd is writable ... yes
Checking whether `c++` is available ... yes
Checking whether `c++` is compatible ... yes
Checking whether arc4random_buf() is available ... yes
Checking whether clock_gettime() is available ... yes
Checking whether getentropy() is available ... yes
Checking whether epoll is available ... yes
Checking whether kqueue is available ... no
Checking whether poll is available ... yes
Warning: You are building a development version. This contains code which has
not been tested as heavily and may contain various faults which could seriously
affect the running of your server. It is recommended that you use a stable
version instead.

You can obtain the latest stable version from https://www.inspircd.org or by
running `git checkout $(git describe --abbrev=0 --tags insp4)` if you are
installing from Git.

I understand this warning and want to continue anyway.
[no] =&gt; yes

Currently, InspIRCd is configured with the following paths:

Binary: /home/builder/Workspaces/inspircd/run/bin
Config: /home/builder/Workspaces/inspircd/run/conf
Data:   /home/builder/Workspaces/inspircd/run/data
Log:    /home/builder/Workspaces/inspircd/run/logs
Manual: /home/builder/Workspaces/inspircd/run/manuals
Module: /home/builder/Workspaces/inspircd/run/modules
Script: /home/builder/Workspaces/inspircd/run

Do you want to change these settings?

[no] =&gt; no

Currently, InspIRCd is configured to automatically enable all available extra modules.

Would you like to enable extra modules manually?

[no] =&gt; no

Enabling the log_syslog module ...
Enabling the regex_posix module ...
Enabling the sslrehashsignal module ...
Creating .configure ...
Writing .configure/cache.cfg ...
Parsing make/template/apparmor ...
Writing .configure/apparmor ...
Parsing make/template/config.h ...
Writing include/config.h ...
Parsing make/template/deploy-ssl.sh ...
Writing .configure/deploy-ssl.sh ...
Parsing make/template/help.txt ...
Writing .configure/help.txt ...
Parsing make/template/inspircd ...
Writing .configure/inspircd ...
Parsing make/template/inspircd-testssl.1 ...
Writing .configure/inspircd-testssl.1 ...
Parsing make/template/inspircd.1 ...
Writing .configure/inspircd.1 ...
Parsing make/template/inspircd.openrc ...
Writing .configure/inspircd.openrc ...
Parsing make/template/inspircd.service ...
Writing .configure/inspircd.service ...
Parsing make/template/logrotate ...
Writing .configure/logrotate ...
Parsing make/template/main.mk ...
Writing GNUmakefile ...
Parsing make/template/org.inspircd.plist ...

Configuration is complete! You have chosen to build with the following settings:

Compiler:
  Binary:  c++
  Name:    GCC
  Version: 13.3

Extra Modules:
  * log_syslog
  * regex_posix
  * ssl_openssl
  * sslrehashsignal

Paths:
  Binary:  /home/builder/Workspaces/inspircd/run/bin
  Config:  /home/builder/Workspaces/inspircd/run/conf
  Data:    /home/builder/Workspaces/inspircd/run/data
  Example: /home/builder/Workspaces/inspircd/run/conf/examples
  Log:     /home/builder/Workspaces/inspircd/run/logs
  Manual:  /home/builder/Workspaces/inspircd/run/manuals
  Module:  /home/builder/Workspaces/inspircd/run/modules
  Runtime: /home/builder/Workspaces/inspircd/run/data
  Script:  /home/builder/Workspaces/inspircd/run

Execution Group: builder (1000)
Execution User:  builder (1000)
Socket Engine:   epoll

To build with these settings run 'make -j17 install' now.
</code></pre></div></div>

<p>I can now make the binaries</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@bosgamerz9:~/Workspaces/inspircd$ make -j17 install
*************************************
*       BUILDING INSPIRCD           *
*                                   *
*   This will take a *long* time.   *
*     Why not read our docs at      *
*     https://docs.inspircd.org     *
*  while you wait for Make to run?  *
*************************************
        BUILD:          channels.cpp
        BUILD:          cull.cpp
        BUILD:          bancache.cpp
        BUILD:          clientprotocol.cpp
        BUILD:          hashcomp.cpp
        BUILD:          configparser.cpp
        BUILD:          dynamic.cpp
        BUILD:          configreader.cpp
        BUILD:          extensible.cpp
        BUILD:          cidr.cpp
        BUILD:          commands.cpp
        BUILD:          channelmanager.cpp
        BUILD:          inspircd.cpp
        BUILD:          helperfuncs.cpp
        BUILD:          listmode.cpp
        BUILD:          base.cpp
        BUILD:          listensocket.cpp
... snip ...


        LINK:           modules/core_xline.so
        LINK:           modules/m_spanningtree.so

*************************************
*        INSTALL COMPLETE!          *
*************************************
Paths:
  Configuration: /home/builder/Workspaces/inspircd/run/conf
  Binaries: /home/builder/Workspaces/inspircd/run/bin
  Modules: /home/builder/Workspaces/inspircd/run/modules
  Data: /home/builder/Workspaces/inspircd/run/data
To start the ircd, run: /home/builder/Workspaces/inspircd/run/inspircd start
Remember to create your config file: /home/builder/Workspaces/inspircd/run/conf/inspircd.conf
Examples are available at: /home/builder/Workspaces/inspircd/run/conf/examples
</code></pre></div></div>

<p>I’m pretty sure that covered install, but I’ll be explicit about it</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@bosgamerz9:~/Workspaces/inspircd$ make install
*************************************
*       BUILDING INSPIRCD           *
*                                   *
*   This will take a *long* time.   *
*     Why not read our docs at      *
*     https://docs.inspircd.org     *
*  while you wait for Make to run?  *
*************************************

*************************************
*        INSTALL COMPLETE!          *
*************************************
Paths:
  Configuration: /home/builder/Workspaces/inspircd/run/conf
  Binaries: /home/builder/Workspaces/inspircd/run/bin
  Modules: /home/builder/Workspaces/inspircd/run/modules
  Data: /home/builder/Workspaces/inspircd/run/data
To start the ircd, run: /home/builder/Workspaces/inspircd/run/inspircd start
Remember to create your config file: /home/builder/Workspaces/inspircd/run/conf/inspircd.conf
Examples are available at: /home/builder/Workspaces/inspircd/run/conf/examples
</code></pre></div></div>

<p>I copied the example conf over and changed things like my name, email and server domain name</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
builder@bosgamerz9:~/Workspaces/inspircd$ cp run/conf/examples/inspircd.example.conf run/conf/inspircd.conf
builder@bosgamerz9:~/Workspaces/inspircd$ vi run/conf/inspircd.conf
</code></pre></div></div>

<p>I’m going to want to use a DNS name (as my IP tends to change from time to time), so I’ll set an A-Record to my current ingress IP</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ az account set --subscription "Pay-As-You-Go" &amp;&amp; az network dns record-set a add-record -g idjdnsrg -z tpk.pw -a 76.156.69.232 -n irc
{
  "ARecords": [
    {
      "ipv4Address": "76.156.69.232"
    }
  ],
  "TTL": 3600,
  "etag": "d06ff40d-eecf-49a6-bad0-fa93b7754588",
  "fqdn": "irc.tpk.pw.",
  "id": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/idjdnsrg/providers/Microsoft.Network/dnszones/tpk.pw/A/irc",
  "name": "irc",
  "provisioningState": "Succeeded",
  "resourceGroup": "idjdnsrg",
  "targetResource": {},
  "trafficManagementProfile": {},
  "type": "Microsoft.Network/dnszones/A"
}
</code></pre></div></div>

<p>And I’ll create a forwarding rule</p>

<p><a href="/content/images/2026/03/irc-01.png"><img src="/content/images/2026/03/irc-01.png" alt="/content/images/2026/03/irc-01.png" /></a></p>

<p>We can now run it</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@bosgamerz9:~/Workspaces/inspircd$ cd run
builder@bosgamerz9:~/Workspaces/inspircd/run$ ls
apparmor  bin  conf  data  deploy-ssl.sh  inspircd  inspircd.openrc  inspircd.service  logrotate  logs  manuals  modules
builder@bosgamerz9:~/Workspaces/inspircd/run$ ./inspircd start
InspIRCd - Internet Relay Chat Daemon
See /INFO for contributors &amp; authors

InspIRCd Process ID: 1243481

Loading core modules .....................
InspIRCd is now running as 'irc.tpk.pw'[247] with 1048576 max open sockets
</code></pre></div></div>

<p>I was having a lot of issues sorting out TLS so I stopped the local process</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@bosgamerz9:~/Workspaces/inspircd/run$ ./inspircd stop
Stopping InspIRCd (pid: 1243481)...
InspIRCd Stopped.
</code></pre></div></div>

<p>And tried using a container (with self-signed certs) instead</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@bosgamerz9:~/Workspaces/inspircd/run$ docker run --name inspircd -p 6667:6667 -p 6697:6697 -e "INSP_TLS_CN=irc.tpk.pw" inspircd/inspircd-dock
er
Unable to find image 'inspircd/inspircd-docker:latest' locally
latest: Pulling from inspircd/inspircd-docker
589002ba0eae: Pull complete
846eed7a3760: Pull complete
c6b8f6e5ebab: Pull complete
720ba1ca8e28: Pull complete
c9d4b0e16ef1: Pull complete
Digest: sha256:5dd8db6d00eeddea7debd5544f38bdbc731500b0d047ae5f24f9f95110a18156
Status: Downloaded newer image for inspircd/inspircd-docker:latest
** Note: You may use '--sec-param High' instead of '--bits 4096'
Generating a 4096 bit RSA private key...
Generating a self signed certificate...
X.509 Certificate Information:
        Version: 3
        Serial Number (hex): 77e3f6ec0d9bd60882c09b73b50962dcacfe35d6
        Validity:
                Not Before: Tue Mar 03 13:13:49 UTC 2026
                Not After: Wed Mar 03 13:13:49 UTC 2027
        Subject: CN=irc.tpk.pw,OU=Example Server Admins,O=Example IRC Network,L=Example City,ST=Example State,C=XZ
        Subject Public Key Algorithm: RSA
        Algorithm Security Level: High (4096 bits)
                Modulus (bits 4096):
                        00:ba:04:c5:bd:9e:cf:4b:7c:89:4a:76:b5:10:af:d5
                        5e:ce:be:17:4f:a8:40:5e:87:06:22:8d:e2:e6:e3:b6
                        c4:50:a7:c5:9c:fe:05:53:9c:20:50:de:56:7e:24:e5
                        57:55:c7:f6:12:7f:c2:e3:b8:07:47:d6:ea:fc:57:d7
                        50:13:e0:68:0a:7c:a0:7c:e6:1d:54:b8:db:ad:bb:56
                        94:37:d5:56:af:9f:9f:8f:29:6f:49:ef:b2:a3:f8:cb
                        d0:75:3c:7c:ff:b6:db:0a:3f:49:cc:7d:9c:57:b1:17
                        e1:7c:e7:3b:aa:84:d5:08:c3:18:64:ae:a7:98:d1:b3
                        47:94:ac:61:89:06:a7:00:d9:33:74:37:d0:4c:af:67
                        03:f0:e0:38:d4:e2:ae:06:b7:0d:44:84:6d:1e:21:f9
                        13:99:95:a3:fd:97:07:f9:1c:bc:e5:d6:8e:bf:10:8c
                        1d:4d:9d:b8:c1:db:ee:cf:0b:c5:be:3a:41:38:da:8c
                        96:f3:2a:be:ea:3f:1e:ac:15:e3:be:19:8c:46:45:9b
                        20:71:62:4d:a0:1a:c1:1a:9a:63:cd:58:d7:5e:29:a9
                        0b:bd:1d:86:fb:66:0f:cd:70:5f:6d:77:7f:ef:a6:90
                        06:01:44:b5:e0:aa:a6:19:7e:b4:d1:2f:22:71:d5:c1
                        5b:74:35:b0:4b:41:2c:86:c8:16:25:cc:b7:f5:db:fc
                        ae:42:72:95:0b:66:be:8d:9b:8a:26:3f:be:7c:83:6a
                        41:fd:a9:f8:58:e9:62:aa:11:63:a7:45:17:1e:25:b9
                        9e:d5:62:21:d8:50:d0:52:22:a3:2c:8f:2d:2c:d2:4c
                        d8:52:74:07:a8:5c:34:43:04:c5:3b:15:de:06:e4:2d
                        01:61:6c:34:4f:ed:f1:94:90:8e:ad:25:67:52:83:c2
                        63:9c:86:7b:b2:bb:78:24:22:5f:f6:6e:35:28:3b:3e
                        6a:1a:a3:00:33:6f:a1:cd:e8:e6:52:23:e3:9e:7f:7b
                        3a:ee:cd:ed:ae:ad:bb:99:67:93:a9:60:49:4a:4b:0b
                        e5:23:c3:a0:06:7e:a9:9b:4e:dc:03:ae:d4:e6:34:36
                        8a:5a:29:e3:c2:5f:85:3a:3f:81:d9:f2:ed:bf:7e:46
                        9b:0a:21:7d:21:91:dc:60:02:a6:64:c5:df:13:21:90
                        44:cd:77:ce:41:6e:68:4e:68:45:7f:17:85:b7:87:8d
                        a6:4a:37:fc:65:ef:16:5f:01:f5:f3:b4:44:e6:93:d7
                        ec:ef:5c:29:17:af:e0:44:50:01:8f:13:95:df:61:0e
                        e8:70:12:9f:54:a2:a3:9d:9a:ea:fd:a2:65:aa:6a:77
                        31
                Exponent (bits 24):
                        01:00:01
        Extensions:
                Basic Constraints (critical):
                        Certificate Authority (CA): FALSE
                Key Purpose (not critical):
                        TLS WWW Client.
                        TLS WWW Server.
                        OCSP signing.
                        Code signing.
                        Time stamping.
                Key Usage (critical):
                        Digital signature.
                        Key encipherment.
                Subject Key Identifier (not critical):
                        1130f4a689411493d7f0baca26131d773c8d8a69
Other Information:
        Public Key ID:
                sha1:1130f4a689411493d7f0baca26131d773c8d8a69
                sha256:8934fdd15907c11f601b82990247e5174870808f3248770169ae112a10f641a7
        Public Key PIN:
                pin-sha256:iTT90VkHwR9gG4KZAkflF0hwgI8ySHcBaa4RKhD2Qac=



Signing certificate...
Generating DH parameters (2048 bits)...
(might take long time)
InspIRCd - Internet Relay Chat Daemon
See /INFO for contributors &amp; authors

InspIRCd Process ID: 1

Loading core modules .....................
[*] Loading module:     m_account.so
[*] Loading module:     m_alias.so
[*] Loading module:     m_allowinvite.so
[*] Loading module:     m_alltime.so
[*] Loading module:     m_banexception.so
[*] Loading module:     m_banredirect.so
[*] Loading module:     m_bcrypt.so
[*] Loading module:     m_blockcolor.so
[*] Loading module:     m_botmode.so
[*] Loading module:     m_callerid.so
[*] Loading module:     m_cap.so
[*] Loading module:     m_chancreate.so
[*] Loading module:     m_chanfilter.so
[*] Loading module:     m_chanhistory.so
[*] Loading module:     m_channelban.so
[*] Loading module:     m_chghost.so
[*] Loading module:     m_chgident.so
[*] Loading module:     m_chgname.so
[*] Loading module:     m_commonchans.so
[*] Loading module:     m_connectban.so
[*] Loading module:     m_connflood.so
[*] Loading module:     m_conn_umodes.so
[*] Loading module:     m_conn_waitpong.so
[*] Loading module:     m_customprefix.so
[*] Loading module:     m_cycle.so
[*] Loading module:     m_dnsbl.so
[*] Loading module:     m_exemptchanops.so
[*] Loading module:     m_filter.so
[*] Loading module:     m_gateway.so
[*] Loading module:     m_globalload.so
[*] Loading module:     m_globops.so
[*] Loading module:     m_help.so
[*] Loading module:     m_hidechans.so
[*] Loading module:     m_inviteexception.so
[*] Loading module:     m_ircv3.so
[*] Loading module:     m_ircv3_accounttag.so
[*] Loading module:     m_ircv3_batch.so
[*] Loading module:     m_ircv3_capnotify.so
[*] Loading module:     m_ircv3_chghost.so
[*] Loading module:     m_ircv3_ctctags.so
[*] Loading module:     m_ircv3_echomessage.so
[*] Loading module:     m_ircv3_invitenotify.so
[*] Loading module:     m_ircv3_labeledresponse.so
[*] Loading module:     m_ircv3_msgid.so
[*] Loading module:     m_ircv3_servertime.so
[*] Loading module:     m_joinflood.so
[*] Loading module:     m_knock.so
[*] Loading module:     m_log_syslog.so
[*] Loading module:     m_messageflood.so
[*] Loading module:     m_monitor.so
[*] Loading module:     m_multiprefix.so
[*] Loading module:     m_noctcp.so
[*] Loading module:     m_nonotice.so
[*] Loading module:     m_operlog.so
[*] Loading module:     m_opermodes.so
[*] Loading module:     m_passforward.so
[*] Loading module:     m_password_hash.so
[*] Loading module:     m_realnameban.so
[*] Loading module:     m_regex_glob.so
[*] Loading module:     m_sajoin.so
[*] Loading module:     m_sakick.so
[*] Loading module:     m_samode.so
[*] Loading module:     m_sanick.so
[*] Loading module:     m_sapart.so
[*] Loading module:     m_saquit.so
[*] Loading module:     m_satopic.so
[*] Loading module:     m_seenicks.so
[*] Loading module:     m_services.so
[*] Loading module:     m_sethost.so
[*] Loading module:     m_setident.so
[*] Loading module:     m_setname.so
[*] Loading module:     m_sha2.so
[*] Loading module:     m_shun.so
[*] Loading module:     m_silence.so
[*] Loading module:     m_spanningtree.so
[*] Loading module:     m_ssl_gnutls.so
[*] Loading module:     m_sslinfo.so
[*] Loading module:     m_sslmodes.so
[*] Loading module:     m_swhois.so
[*] Loading module:     m_tline.so
[*] Loading module:     m_uhnames.so
[*] Loading module:     m_xline_db.so
InspIRCd is now running as '6c2f16f4e983.example.com'[387] with 1048576 max open sockets
</code></pre></div></div>
<!--

A close-up shot of a clown in a hat, his wild eyes look at the camera but dart back and forth with a sinister smile.  The camera begins slightly to his side, the slowly pulls back revealing a middle aged software engineer standing nearby.   The clown looks at the man then they start to fight, punching and hitting each other

-->

<p>I struggled for a while getting TLS to work and after enough time, decided a containerized approach would be more fruitful.</p>

<h2 id="inspircd-docker">inspircd-docker</h2>

<p>I found <a href="https://hub.docker.com/r/inspircd/inspircd-docker/">this inspircd docker container</a> that would use TLS with self-signed certs.</p>

<p>While I would prefer properly signed, I figured this was a good start.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ docker run --name inspircd -p 6667:6667 -p 6697:6697 -e "INSP_TLS_CN=irc.tpk.pw" inspircd/inspircd-docker
</code></pre></div></div>

<p>We already had a forwarding rule set earlier for that DNS name.</p>

<p>I can use an app like xxxx on my phone to now connect.</p>

<p>It will warn about the cert, but I just need to accept</p>

<p><a href="/content/images/2026/03/irc-02.png"><img src="/content/images/2026/03/irc-02.png" alt="/content/images/2026/03/irc-02.png" /></a></p>

<p>but then it works just fine</p>

<p><a href="/content/images/2026/03/irc-03.png"><img src="/content/images/2026/03/irc-03.png" alt="/content/images/2026/03/irc-03.png" /></a></p>

<h2 id="scripting-output">Scripting output</h2>

<p>I wanted to be able to post messages from processes and builds.</p>

<p>It took a few iterations to sort out TCP piping, but I managed to get it finall work:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">cat</span> ./testirc3.sh
<span class="c">#!/bin/bash</span>

<span class="c"># Configuration</span>
<span class="nv">SERVER</span><span class="o">=</span><span class="s2">"irc.tpk.pw"</span>
<span class="nv">PORT</span><span class="o">=</span><span class="s2">"6667"</span>
<span class="nv">NICK</span><span class="o">=</span><span class="s2">"mybot"</span>
<span class="nv">CHAN</span><span class="o">=</span><span class="s2">"#general"</span>

<span class="c"># 1. Open a bidirectional socket on File Descriptor 3</span>
<span class="nb">exec </span>3&lt;<span class="o">&gt;</span>/dev/tcp/<span class="nv">$SERVER</span>/<span class="nv">$PORT</span>

<span class="c"># 2. Send registration info to the socket</span>
<span class="nb">echo</span> <span class="s2">"NICK </span><span class="nv">$NICK</span><span class="s2">"</span> <span class="o">&gt;</span>&amp;3
<span class="nb">echo</span> <span class="s2">"USER </span><span class="nv">$NICK</span><span class="s2"> 8 * :My Bot"</span> <span class="o">&gt;</span>&amp;3

<span class="c"># 3. Read from the socket (Descriptor 3)</span>
<span class="k">while </span><span class="nb">read</span> <span class="nt">-u</span> 3 line<span class="p">;</span> <span class="k">do</span>
    <span class="c"># Print server output to your terminal so you can see what's happening</span>
    <span class="nb">echo</span> <span class="s2">"&lt;&lt; </span><span class="nv">$line</span><span class="s2">"</span>

    <span class="k">case</span> <span class="s2">"</span><span class="nv">$line</span><span class="s2">"</span> <span class="k">in</span>
        <span class="c"># Answer the PING immediately</span>
        PING<span class="k">*</span><span class="p">)</span>
            <span class="nv">ID</span><span class="o">=</span><span class="si">$(</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$line</span><span class="s2">"</span> | <span class="nb">cut</span> <span class="nt">-d</span><span class="s1">':'</span> <span class="nt">-f2</span><span class="si">)</span>
            <span class="nb">echo</span> <span class="s2">"PONG :</span><span class="nv">$ID</span><span class="s2">"</span> <span class="o">&gt;</span>&amp;3
            <span class="nb">echo</span> <span class="s2">"&gt;&gt; PONG sent"</span>
            <span class="p">;;</span>

        <span class="c"># 001 is the "Welcome" numeric. Now we can join and talk.</span>
        <span class="k">*</span>001<span class="k">*</span><span class="p">)</span>
            <span class="nb">echo</span> <span class="s2">"JOIN </span><span class="nv">$CHAN</span><span class="s2">"</span> <span class="o">&gt;</span>&amp;3
            <span class="nb">sleep </span>2 <span class="c"># Small buffer for the server to process the join</span>
            <span class="nb">echo</span> <span class="s2">"PRIVMSG </span><span class="nv">$CHAN</span><span class="s2"> :Hello from the command line!"</span> <span class="o">&gt;</span>&amp;3
            <span class="nb">echo</span> <span class="s2">"QUIT"</span> <span class="o">&gt;</span>&amp;3
            <span class="nb">echo</span> <span class="s2">"&gt;&gt; Message sent and quitting."</span>
            <span class="nb">exit</span>
            <span class="p">;;</span>

        <span class="c"># Error handling if the nick is taken</span>
        <span class="k">*</span>433<span class="k">*</span><span class="p">)</span>
            <span class="nb">echo</span> <span class="s2">"Error: Nickname already in use."</span>
            <span class="nb">exit </span>1
            <span class="p">;;</span>
    <span class="k">esac</span>
<span class="k">done</span>
</code></pre></div></div>

<p>Testing shows a completed message</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces$ ./testirc3.sh
&lt;&lt; :6c2f16f4e983.example.com NOTICE * :*** Looking up your hostname...
&lt;&lt; PING :yvof8p1ksK
&gt;&gt; PONG sent
&lt;&lt; :6c2f16f4e983.example.com NOTICE mybot :*** Found your hostname (RT-BE92U-BDA0)
&lt;&lt; :6c2f16f4e983.example.com 001 mybot :Welcome to the ExampleNet IRC Network mybot!mybot@RT-BE92U-BDA0
&gt;&gt; Message sent and quitting.
</code></pre></div></div>

<p>We can now see it post (i tested twice, it did not post twice).</p>

<p><a href="/content/images/2026/03/irc-04.png"><img src="/content/images/2026/03/irc-04.png" alt="/content/images/2026/03/irc-04.png" /></a></p>

<p>Having multiple devices try to use my username ended up with a ban I could not solve so I reset the container and connected, this time giving a unique name to each device.</p>

<p>But it did come up and work just fine</p>

<p><a href="/content/images/2026/03/irc-05.png"><img src="/content/images/2026/03/irc-05.png" alt="/content/images/2026/03/irc-05.png" /></a></p>

<h2 id="matrix">Matrix</h2>

<p>I don’t often use it - and really, I’m not sure why - perhaps it’s because it’s rather dead without users, but I do have a Matrix Synapse host (matrix.freshbrewed.science).</p>

<p>All my build scripts keep posting there</p>

<p><a href="/content/images/2026/03/irc-06.png"><img src="/content/images/2026/03/irc-06.png" alt="/content/images/2026/03/irc-06.png" /></a></p>

<p>I looked at my last deploy and see it was from a few years ago (good testament to something that doesnt fall down)</p>

<blockquote>
  <p>$ helm list | grep matrix
matrix-synapse                  default         1               2024-03-02 08:37:10.310138818 -0600 CST deployed        matrix-synapse-3.8.2                   1.101.0</p>
</blockquote>

<p>Seems the latest chart is up to 1.148.0 (from my 1.101.0)</p>

<p>The upgrade should be just as easy as it was on 2024-01-30…</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ helm upgrade matrix-synapse ananace-charts/matrix-synapse --set serverName=matrix.freshbrewed.science --set wellknown.enabled=true
Release "matrix-synapse" has been upgraded. Happy Helming!
NAME: matrix-synapse
LAST DEPLOYED: Wed Mar 11 10:27:54 2026
NAMESPACE: default
STATUS: deployed
REVISION: 2
NOTES:
** Note, this chart may take a while to finish setup, please be patient **
** Also, remember to disable the signingkey job (signingkey.job.enabled=false) **

Your Synapse install is now starting, you should soon be able to access it on
the following URL(s);
http://matrix.freshbrewed.science

You can create a user in your new Synapse install by running the following
command; (replacing USERNAME and PASSWORD)

    export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=matrix-synapse,app.kubernetes.io/instance=matrix-synapse,app.kubernetes.io/component=synapse" -o jsonpath="{.items[0].metadata.name}")
    kubectl exec --namespace default $POD_NAME -- register_new_matrix_user -c /synapse/config/homeserver.yaml -c /synapse/config/conf.d/secrets.yaml -u USERNAME -p PASSWORD --admin http://localhost:8008

You can also specify --no-admin to create a non-admin user.
</code></pre></div></div>

<p>As I watched containers, I could see there was a bit of an order issue - as the PostgreSQL container rotated after the app, the app got into a crash loop for a few moments. but then it came back</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>builder@DESKTOP-QADGF36:~/Workspaces$ kubectl get po | grep matrix
matrix-synapse-55f7b5dbc5-8xzc8                      0/1     Running            1 (2s ago)        19s
matrix-synapse-bfc9b99b7-2gdzp                       1/1     Running            398 (6d5h ago)    207d
matrix-synapse-postgresql-0                          1/1     Terminating        6 (17d ago)       207d
matrix-synapse-redis-master-6986bdf45b-8w6tx         0/1     Running            0                 19s
matrix-synapse-redis-master-8f95f8bd7-p5ptf          1/1     Running            6 (17d ago)       185d
matrix-synapse-wellknown-lighttpd-7496b664b4-7wfxf   1/1     Running            0                 19s
builder@DESKTOP-QADGF36:~/Workspaces$ kubectl get po | grep matrix
matrix-synapse-55f7b5dbc5-8xzc8                      0/1     Error              1 (13s ago)        30s
matrix-synapse-bfc9b99b7-2gdzp                       1/1     Running            398 (6d5h ago)     207d
matrix-synapse-postgresql-0                          1/1     Terminating        6 (17d ago)        207d
matrix-synapse-redis-master-6986bdf45b-8w6tx         1/1     Running            0                  30s
matrix-synapse-wellknown-lighttpd-7496b664b4-7wfxf   1/1     Running            0                  30s
builder@DESKTOP-QADGF36:~/Workspaces$ kubectl get po | grep matrix
matrix-synapse-55f7b5dbc5-8xzc8                      0/1     CrashLoopBackOff   1 (10s ago)        36s
matrix-synapse-bfc9b99b7-2gdzp                       1/1     Running            398 (6d5h ago)     207d
matrix-synapse-postgresql-0                          0/1     Running            0                  5s
matrix-synapse-redis-master-6986bdf45b-8w6tx         1/1     Running            0                  36s
matrix-synapse-wellknown-lighttpd-7496b664b4-7wfxf   1/1     Running            0                  36s
builder@DESKTOP-QADGF36:~/Workspaces$ kubectl get po | grep matrix
matrix-synapse-55f7b5dbc5-8xzc8                      0/1     Running            2 (22s ago)        48s
matrix-synapse-bfc9b99b7-2gdzp                       1/1     Running            398 (6d5h ago)     207d
matrix-synapse-postgresql-0                          1/1     Running            0                  17s
matrix-synapse-redis-master-6986bdf45b-8w6tx         1/1     Running            0                  48s
matrix-synapse-wellknown-lighttpd-7496b664b4-7wfxf   1/1     Running            0                  48s
builder@DESKTOP-QADGF36:~/Workspaces$ kubectl get po | grep matrix
matrix-synapse-55f7b5dbc5-8xzc8                      1/1     Running            2 (75s ago)        101s
matrix-synapse-postgresql-0                          1/1     Running            0                  70s
matrix-synapse-redis-master-6986bdf45b-8w6tx         1/1     Running            0                  101s
matrix-synapse-wellknown-lighttpd-7496b664b4-7wfxf   1/1     Running            0                  101s
</code></pre></div></div>

<p>A refresh of Cinny showed the room was up and running</p>

<p><a href="/content/images/2026/03/irc-07.png"><img src="/content/images/2026/03/irc-07.png" alt="/content/images/2026/03/irc-07.png" /></a></p>

<p>while I don’t have Cinny exposed externally, I do have element available to all, which has a container set to “latest” so it’s always up to date</p>

<p><a href="/content/images/2026/03/irc-08.png"><img src="/content/images/2026/03/irc-08.png" alt="/content/images/2026/03/irc-08.png" /></a></p>

<h1 id="summary">Summary</h1>

<p>IRC is still very cool for chat.  I think there is more work on my end to figure out how to apply controls (logins, admins, proper SSL).</p>

<p>That said, I’m not certain I need anything beyond Matrix with some proper Matrix clients.  Most of my other threads come through Discord, Google Chat, Keybase and Telegram.</p>

<p>What is funny to me is we used to have <a href="https://www.pidgin.im/">Pidgin</a> and <a href="https://en.wikipedia.org/wiki/Trillian_(software)">Trillium</a> long long ago back when there were <em>too many</em> chat options and we wanted a single client that could bridge gchat, yahoo chat, aolchat, ICQ, MSN and more.. Seems we may need to come back to that again.</p>

<p>Side note: Just looking at <a href="https://www.pidgin.im/plugins/">the Pidgin plugins</a>, I think I have a winner for the future - it coveres all of mine (save for Keybase.io).</p>]]></content><author><name>Isaac Johnson</name></author><category term="Chat" /><category term="Matrix" /><category term="IRC" /><category term="Opensource" /><category term="containers" /><category term="kubernetes" /><summary type="html"><![CDATA[Let’s talk about Chat clients. I got to wondering what every happened to IRC. Internet Relay Chat (IRC) has been around since 1988, but I certainly got to know it later in 1996. It was my usual hangout space in the early 2000s but maybe I just got busy - I stopped using it in the ’10s. It never went away. It hasn’t changed at all and only recently, after about 22 years did people hop off Freenode to Libera.chat as the primary server.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://freshbrewed.science/content/images/2026/03/20240807_191311.jpg" /><media:content medium="image" url="https://freshbrewed.science/content/images/2026/03/20240807_191311.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Misc Apps: Toolstack and Homehub</title><link href="https://freshbrewed.science/2026/03/10/miscapps.html" rel="alternate" type="text/html" title="Misc Apps: Toolstack and Homehub" /><published>2026-03-10T01:01:01+00:00</published><updated>2026-03-10T01:01:01+00:00</updated><id>https://freshbrewed.science/2026/03/10/miscapps</id><content type="html" xml:base="https://freshbrewed.science/2026/03/10/miscapps.html"><![CDATA[<p>I had bookmarked <a href="https://mariushosting.com/how-to-install-homehub-on-your-synology-nas/">this Marius post</a> on <a href="https://github.com/surajverma/homehub">Homehub</a>, a nice family hub app.  I wanted to check that out and maybe see if I could tweak it a bit.</p>

<p>I also wanted to look at <a href="https://github.com/surajverma/toolstack">Toolstack</a>, another of that authors public repos that seemed to be a rather clever assortment of useful utilities.</p>

<p>Let’s start with that.</p>

<h1 id="toolstack">Toolstack</h1>

<p>I found this <a href="https://github.com/surajverma/toolstack">interesting Github repo</a> from <a href="https://www.surajverma.in/">Suraj</a> that had a bunch of utilities served up via a web browser.</p>

<p>Let’s just start with cloning the repo down</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(base) builder@LuiGi:~/Workspaces$ git clone https://github.com/surajverma/toolstack.git
Cloning into 'toolstack'...
remote: Enumerating objects: 196, done.
remote: Counting objects: 100% (196/196), done.
remote: Compressing objects: 100% (84/84), done.
remote: Total 196 (delta 67), reused 186 (delta 58), pack-reused 0 (from 0)
Receiving objects: 100% (196/196), 275.17 KiB | 304.00 KiB/s, done.
Resolving deltas: 100% (67/67), done.
(base) builder@LuiGi:~/Workspaces$ nvm use lts/jod
Now using node v22.22.0 (npm v10.9.4)
(base) builder@LuiGi:~/Workspaces$ cd toolstack/
</code></pre></div></div>

<p>Then installing any dependencies</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(base) builder@LuiGi:~/Workspaces/toolstack$ npm install

added 375 packages, and audited 376 packages in 13s

141 packages are looking for funding
  run `npm fund` for details

8 vulnerabilities (3 low, 2 moderate, 2 high, 1 critical)

To address issues that do not require attention, run:
  npm audit fix

To address all issues, run:
  npm audit fix --force

Run `npm audit` for details.
</code></pre></div></div>

<p>I can now fire it up in dev mode (<code class="language-plaintext highlighter-rouge">npm run dev</code>)</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(base) builder@LuiGi:~/Workspaces/toolstack$ npm run dev

&gt; toolstack@0.1.0 dev
&gt; next dev --turbopack

   ▲ Next.js 15.3.3 (Turbopack)
   - Local:        http://localhost:3000
   - Network:      http://10.239.189.22:3000

 ✓ Starting...
Attention: Next.js now collects completely anonymous telemetry regarding usage.
This information is used to shape Next.js' roadmap and prioritize features.
You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:
https://nextjs.org/telemetry

 ✓ Ready in 727ms
</code></pre></div></div>

<p>It is actually loaded with little utilities</p>

<video muted="" controls="">
    <source src="/content/images/2026/03/toolstack-01.mp4" type="video/mp4" />
</video>

<p>I like this, but wish I could just put it out on a URL and enjoy it (instead of running NodeJS locally).</p>

<p>I put together a simple Dockerfile</p>
<div class="language-Dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="w"> </span><span class="s">node:20-alpine</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">production</span>
<span class="k">WORKDIR</span><span class="s"> /app</span>
<span class="k">COPY</span><span class="s"> . .</span>
<span class="k">EXPOSE</span><span class="s"> 3000</span>
<span class="k">CMD</span><span class="s"> ["npm", "run", "dev", "--", "-H", "0.0.0.0"]</span>
</code></pre></div></div>

<p>Then build and tested</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(base) builder@LuiGi:~/Workspaces/toolstack$ docker build -t mytest:04 .
[+] Building 4.5s (9/9) FINISHED                                                       docker:default
 =&gt; [internal] load build definition from Dockerfile                                             0.0s
 =&gt; =&gt; transferring dockerfile: 170B                                                             0.0s
 =&gt; [internal] load metadata for docker.io/library/node:20-alpine                                1.1s
 =&gt; [auth] library/node:pull token for registry-1.docker.io                                      0.0s
 =&gt; [internal] load .dockerignore                                                                0.0s
 =&gt; =&gt; transferring context: 2B                                                                  0.0s
 =&gt; [1/3] FROM docker.io/library/node:20-alpine@sha256:09e2b3d9726018aecf269bd35325f46bf75046a6  0.0s
 =&gt; [internal] load build context                                                                0.5s
 =&gt; =&gt; transferring context: 1.71MB                                                              0.5s
 =&gt; CACHED [2/3] WORKDIR /app                                                                    0.0s
 =&gt; [3/3] COPY . .                                                                               1.2s
 =&gt; exporting to image                                                                           1.6s
 =&gt; =&gt; exporting layers                                                                          1.5s
 =&gt; =&gt; writing image sha256:1870f773c57fb4c01a76ec4f0d67dab1a4482c6889e30418d9df18fbb1178760     0.0s
 =&gt; =&gt; naming to docker.io/library/mytest:04                                                     0.0s
(base) builder@LuiGi:~/Workspaces/toolstack$ docker run -p 3000:3000 mytest:04

&gt; toolstack@0.1.0 dev
&gt; next dev --turbopack -H 0.0.0.0

   ▲ Next.js 15.3.3 (Turbopack)
   - Local:        http://localhost:3000
   - Network:      http://0.0.0.0:3000

 ✓ Starting...
Attention: Next.js now collects completely anonymous telemetry regarding usage.
This information is used to shape Next.js' roadmap and prioritize features.
You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:
https://nextjs.org/telemetry

 ✓ Ready in 890ms
 ○ Compiling / ...
 ✓ Compiled / in 6.5s
 GET / 200 in 6788ms
</code></pre></div></div>

<p>It’s a pretty simple app, so I’ll just push it up to dockerhub</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(venv) (base) builder@LuiGi:~/Workspaces/toolstack$ docker tag mytest:04 idjohnson/toolstack:01
(venv) (base) builder@LuiGi:~/Workspaces/toolstack$ docker push idjohnson/toolstack:01
The push refers to repository [docker.io/idjohnson/toolstack]
3042ce71e8c5: Pushed
5e2c1c4da1e2: Pushed
1dcaf7a5a25b: Mounted from library/node
3bcb0b085764: Mounted from library/node
e881f55858a8: Mounted from library/node
989e799e6349: Mounted from library/node
01: digest: sha256:e5b56fe4eb79665a441a8766ab2a93cf5421165ff4b6a18e83d9b1924ee20b7d size: 1577
</code></pre></div></div>

<p>I’ll create a quick A record</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ az account set --subscription "Pay-As-You-Go" &amp;&amp; az network dns record-set a add-record -g idjdnsrg -z tpk.pw -a 76.156.69.232 -n toolbox
{
  "ARecords": [
    {
      "ipv4Address": "76.156.69.232"
    }
  ],
  "TTL": 3600,
  "etag": "a97f4aeb-5bd9-4b26-ac3e-3e5cb5518305",
  "fqdn": "toolbox.tpk.pw.",
  "id": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/idjdnsrg/providers/Microsoft.Network/dnszones/tpk.pw/A/toolbox",
  "name": "toolbox",
  "provisioningState": "Succeeded",
  "resourceGroup": "idjdnsrg",
  "targetResource": {},
  "trafficManagementProfile": {},
  "type": "Microsoft.Network/dnszones/A"
}
</code></pre></div></div>

<p>Now I just need a service, deployment and ingress to use that DNS entry</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">(venv) (base) builder@LuiGi:~/Workspaces/toolstack$ cat ./manifest.yaml</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">toolstack-deployment</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">2</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">toolstack</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app</span><span class="pi">:</span> <span class="s">toolstack</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">toolstack-container</span>
        <span class="na">image</span><span class="pi">:</span> <span class="s">idjohnson/toolstack:01</span>
        <span class="na">ports</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">containerPort</span><span class="pi">:</span> <span class="m">3000</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">toolstack-service</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">app</span><span class="pi">:</span> <span class="s">toolstack</span>
  <span class="na">ports</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">protocol</span><span class="pi">:</span> <span class="s">TCP</span>
      <span class="na">port</span><span class="pi">:</span> <span class="m">80</span>
      <span class="na">targetPort</span><span class="pi">:</span> <span class="m">3000</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">networking.k8s.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Ingress</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">annotations</span><span class="pi">:</span>
    <span class="na">cert-manager.io/cluster-issuer</span><span class="pi">:</span> <span class="s">azuredns-tpkpw</span>
    <span class="na">ingress.kubernetes.io/ssl-redirect</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
    <span class="na">kubernetes.io/ingress.class</span><span class="pi">:</span> <span class="s">nginx</span>
    <span class="na">kubernetes.io/tls-acme</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
    <span class="na">nginx.ingress.kubernetes.io/proxy-body-size</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0"</span>
    <span class="na">nginx.ingress.kubernetes.io/proxy-read-timeout</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3600"</span>
    <span class="na">nginx.ingress.kubernetes.io/proxy-send-timeout</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3600"</span>
    <span class="na">nginx.ingress.kubernetes.io/ssl-redirect</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
    <span class="na">nginx.org/client-max-body-size</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0"</span>
    <span class="na">nginx.org/proxy-connect-timeout</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3600"</span>
    <span class="na">nginx.org/proxy-read-timeout</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3600"</span>
    <span class="na">nginx.org/websocket-services</span><span class="pi">:</span> <span class="s">toolstack-service</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">toolstack-ingress</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">rules</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">host</span><span class="pi">:</span> <span class="s">toolbox.tpk.pw</span>
    <span class="na">http</span><span class="pi">:</span>
      <span class="na">paths</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">backend</span><span class="pi">:</span>
          <span class="na">service</span><span class="pi">:</span>
            <span class="na">name</span><span class="pi">:</span> <span class="s">toolstack-service</span>
            <span class="na">port</span><span class="pi">:</span>
              <span class="na">number</span><span class="pi">:</span> <span class="m">80</span>
        <span class="na">path</span><span class="pi">:</span> <span class="s">/</span>
        <span class="na">pathType</span><span class="pi">:</span> <span class="s">Prefix</span>
  <span class="na">tls</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">hosts</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">toolbox.tpk.pw</span>
    <span class="na">secretName</span><span class="pi">:</span> <span class="s">toolbox-tls</span>
</code></pre></div></div>

<p>And apply them in k8s</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(venv) (base) builder@LuiGi:~/Workspaces/toolstack$ kubectl apply -f ./manifest.yaml
deployment.apps/toolstack-deployment created
service/toolstack-service created
Warning: annotation "kubernetes.io/ingress.class" is deprecated, please use 'spec.ingressClassName' instead
ingress.networking.k8s.io/toolstack-ingress created
</code></pre></div></div>

<p>Once the cert is ready</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get cert | grep tool
toolbox-tls                         True    toolbox-tls                         90s
</code></pre></div></div>

<p>I can now access <a href="https://toolbox.tpk.pw">https://toolbox.tpk.pw</a></p>

<p><a href="/content/images/2026/03/toolstack-03.png"><img src="/content/images/2026/03/toolstack-03.png" alt="/content/images/2026/03/toolstack-03.png" /></a></p>

<h1 id="homehub">Homehub</h1>

<p>Another of his projects is <a href="https://github.com/surajverma/homehub">Homehub</a> which like the toolbox, is a catch all for a slew of tools a home might want to share amongst the family unit.</p>

<p>This one came with a container as well as a docker compose</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># compose.yml</span>
<span class="na">services</span><span class="pi">:</span>
  <span class="na">homehub</span><span class="pi">:</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">homehub</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">ghcr.io/surajverma/homehub:latest</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">5000:5000"</span> <span class="c1">#app listens internally on port 5000</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">FLASK_ENV=production</span>
      <span class="pi">-</span> <span class="s">SECRET_KEY=${SECRET_KEY:-}</span> <span class="c1"># set via .env; falls back to random if not provided</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">./uploads:/app/uploads</span>
      <span class="pi">-</span> <span class="s">./media:/app/media</span>
      <span class="pi">-</span> <span class="s">./pdfs:/app/pdfs</span>
      <span class="pi">-</span> <span class="s">./data:/app/data</span>
      <span class="pi">-</span> <span class="s">./config.yml:/app/config.yml:ro</span>
</code></pre></div></div>

<p>I’ll start by cloning the repo</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(venv) (base) builder@LuiGi:~/Workspaces$ git clone https://github.com/surajverma/homehub.git
Cloning into 'homehub'...
remote: Enumerating objects: 487, done.
remote: Counting objects: 100% (116/116), done.
remote: Compressing objects: 100% (57/57), done.
remote: Total 487 (delta 86), reused 59 (delta 59), pack-reused 371 (from 2)
Receiving objects: 100% (487/487), 355.90 KiB | 3.46 MiB/s, done.
Resolving deltas: 100% (284/284), done.

</code></pre></div></div>

<p>We then need to copy over the example YAML config file</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(venv) (base) builder@LuiGi:~/Workspaces$ cd homehub/
(venv) (base) builder@LuiGi:~/Workspaces/homehub$ ls
Dockerfile  README.md  compose.yml         package.json      run.py  tailwind.config.js  tests
LICENSE     app        config-example.yml  requirements.txt  static  templates           wsgi.py
(venv) (base) builder@LuiGi:~/Workspaces/homehub$ cp config-example.yml config.yml
</code></pre></div></div>

<p>Once I edited the file, i just did a <code class="language-plaintext highlighter-rouge">docker compose up</code> to check it out</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(venv) (base) builder@LuiGi:~/Workspaces/homehub$ vi config.yml
(venv) (base) builder@LuiGi:~/Workspaces/homehub$ docker compose up
[+] Running 11/11
 ✔ homehub Pulled                                                                                                                        7.0s
   ✔ 1074353eec0d Already exists                                                                                                         0.0s
   ✔ 9e4742745279 Pull complete                                                                                                          0.6s
   ✔ 522560f13f9b Pull complete                                                                                                          1.3s
   ✔ a4581c766c38 Pull complete                                                                                                          1.4s
   ✔ 38b0eea34756 Pull complete                                                                                                          1.5s
   ✔ 4147244d0bd5 Pull complete                                                                                                          4.9s
   ✔ 70974fc3d2fc Pull complete                                                                                                          5.7s
   ✔ c68d49ee74e4 Pull complete                                                                                                          5.7s
   ✔ c726830993c6 Pull complete                                                                                                          5.8s
   ✔ d6e2f8e4672e Pull complete                                                                                                          5.9s
[+] Running 2/2
 ✔ Network homehub_default  Created                                                                                                      0.1s
 ✔ Container homehub        Created                                                                                                      0.2s
Attaching to homehub
homehub  | [2026-03-04 00:09:03 +0000] [1] [INFO] Starting gunicorn 23.0.0
homehub  | [2026-03-04 00:09:03 +0000] [1] [INFO] Listening at: http://0.0.0.0:5000 (1)
homehub  | [2026-03-04 00:09:03 +0000] [1] [INFO] Using worker: sync
homehub  | [2026-03-04 00:09:03 +0000] [7] [INFO] Booting worker with pid: 7
</code></pre></div></div>

<p>I can access it on port 5000</p>

<p><a href="/content/images/2026/03/homehub-01.png"><img src="/content/images/2026/03/homehub-01.png" alt="/content/images/2026/03/homehub-01.png" /></a></p>

<p>We can mark ourselves home or set a status or event</p>

<p><a href="/content/images/2026/03/homehub-02.png"><img src="/content/images/2026/03/homehub-02.png" alt="/content/images/2026/03/homehub-02.png" /></a></p>

<p>The shared cloud is a nice place to just stash files for sharing</p>

<p><a href="/content/images/2026/03/homehub-03.png"><img src="/content/images/2026/03/homehub-03.png" alt="/content/images/2026/03/homehub-03.png" /></a></p>

<p>The shopping list creates a nice place to add grocery lists (a bit of our own here)</p>

<p><a href="/content/images/2026/03/homehub-04.png"><img src="/content/images/2026/03/homehub-04.png" alt="/content/images/2026/03/homehub-04.png" /></a></p>

<p>chores</p>

<p><a href="/content/images/2026/03/homehub-05.png"><img src="/content/images/2026/03/homehub-05.png" alt="/content/images/2026/03/homehub-05.png" /></a></p>

<p>Recipes</p>

<p><a href="/content/images/2026/03/homehub-06.png"><img src="/content/images/2026/03/homehub-06.png" alt="/content/images/2026/03/homehub-06.png" /></a></p>

<p>Expiry - I could actually use this as we do dance between a lot of subscription services</p>

<p><a href="/content/images/2026/03/homehub-07.png"><img src="/content/images/2026/03/homehub-07.png" alt="/content/images/2026/03/homehub-07.png" /></a></p>

<p>The URL Shortener, Media Downloader and PDF Compressor do not really seem like the kind of things we would need.</p>

<p>But the Wifi QR code generator would be nice</p>

<p><a href="/content/images/2026/03/homehub-08.png"><img src="/content/images/2026/03/homehub-08.png" alt="/content/images/2026/03/homehub-08.png" /></a></p>

<p>And I could imagine an expense tracker to be used, if anything, by the kiddos seeking reimbursement (e.g. fill car with gas, take out sister for dinner, etc)</p>

<p><a href="/content/images/2026/03/homehub-09.png"><img src="/content/images/2026/03/homehub-09.png" alt="/content/images/2026/03/homehub-09.png" /></a></p>

<p>Now, here is a bit of the rub.  This has no auth.  And without auth, there is no way I would externalize my home WIFI details, nor our expenses.  Thus, as it stands, I might very well just use on a local dockerhost.</p>

<h2 id="gemini">Gemini</h2>

<p>But it had me wondering - how hard would it be to add some basic auth in there.</p>

<p><a href="/content/images/2026/03/homehub-10.png"><img src="/content/images/2026/03/homehub-10.png" alt="/content/images/2026/03/homehub-10.png" /></a></p>

<p>Gemini updated the YAML and provided some pytests to go with it.</p>

<p>Next was to test building a local image</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(venv) (base) builder@LuiGi:~/Workspaces/homehub$ docker build -t homehub:test1 .
[+] Building 44.6s (20/20) FINISHED                                                                                            docker:default
 =&gt; [internal] load build definition from Dockerfile                                                                                     0.1s
 =&gt; =&gt; transferring dockerfile: 1.24kB                                                                                                   0.0s
 =&gt; [internal] load metadata for docker.io/library/python:3.12-alpine                                                                    2.4s
 =&gt; [auth] library/python:pull token for registry-1.docker.io                                                                            0.0s
 =&gt; [internal] load .dockerignore                                                                                                        0.1s
 =&gt; =&gt; transferring context: 222B                                                                                                        0.0s
 =&gt; [internal] load build context                                                                                                        0.1s
 =&gt; =&gt; transferring context: 797.25kB                                                                                                    0.0s
 =&gt; [builder 1/9] FROM docker.io/library/python:3.12-alpine@sha256:53e7c5ca6ceff2fc02e9b4b0e7cddb7ce8fa9683dedb89fdf73cab1423079c15      2.2s
 =&gt; =&gt; resolve docker.io/library/python:3.12-alpine@sha256:53e7c5ca6ceff2fc02e9b4b0e7cddb7ce8fa9683dedb89fdf73cab1423079c15              0.0s
 =&gt; =&gt; sha256:53e7c5ca6ceff2fc02e9b4b0e7cddb7ce8fa9683dedb89fdf73cab1423079c15 10.30kB / 10.30kB                                         0.0s
 =&gt; =&gt; sha256:1d132f2d802114876d114b0918e01998bf0c07af8332f776c3c4cb8e388c9a5f 1.74kB / 1.74kB                                           0.0s
 =&gt; =&gt; sha256:e31482fb1094d66a7c2082cd69202658e2f1f8da5b9e455caae09f1e27cdaf9f 5.42kB / 5.42kB                                           0.0s
 =&gt; =&gt; sha256:6de7811bee64ba64355c278198dcf2e22b4efba35aa596f23f6ed80278ac5afe 460.95kB / 460.95kB                                       0.6s
 =&gt; =&gt; sha256:6f3f43492c5b00f17f4a416bd78474116d69f3840b50bbc51c6654e4a4f88aa2 13.74MB / 13.74MB                                         1.6s
 =&gt; =&gt; sha256:89a8227ba99bcd9d5df82f6cb4e5ea1b222c3ec33685ba1a0d8c6db402f012ad 247B / 247B                                               0.5s
 =&gt; =&gt; extracting sha256:6de7811bee64ba64355c278198dcf2e22b4efba35aa596f23f6ed80278ac5afe                                                0.1s
 =&gt; =&gt; extracting sha256:6f3f43492c5b00f17f4a416bd78474116d69f3840b50bbc51c6654e4a4f88aa2                                                0.3s
 =&gt; =&gt; extracting sha256:89a8227ba99bcd9d5df82f6cb4e5ea1b222c3ec33685ba1a0d8c6db402f012ad                                                0.0s
 =&gt; [builder 2/9] WORKDIR /app                                                                                                           0.1s
 =&gt; [builder 3/9] RUN apk add --no-cache     build-base     zlib-dev     jpeg-dev     nodejs     npm                                     9.8s
 =&gt; [stage-1 3/7] RUN apk add --no-cache     ffmpeg     ghostscript     libjpeg-turbo     zlib     libstdc++                            14.5s
 =&gt; [builder 4/9] COPY requirements.txt .                                                                                                0.1s
 =&gt; [builder 5/9] RUN pip install --no-cache-dir -r requirements.txt                                                                    19.7s
 =&gt; [builder 6/9] COPY package.json tailwind.config.js ./                                                                                0.1s
 =&gt; [builder 7/9] COPY static/input.css ./static/                                                                                        0.1s
 =&gt; [builder 8/9] COPY templates ./templates                                                                                             0.1s
 =&gt; [builder 9/9] RUN npm install &amp;&amp; npm run build:css                                                                                   7.4s
 =&gt; [stage-1 4/7] COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages                    1.1s
 =&gt; [stage-1 5/7] COPY --from=builder /usr/local/bin /usr/local/bin                                                                      0.1s
 =&gt; [stage-1 6/7] COPY . /app                                                                                                            0.1s
 =&gt; [stage-1 7/7] COPY --from=builder /app/static/output.css /app/static/output.css                                                      0.1s
 =&gt; exporting to image                                                                                                                   0.7s
 =&gt; =&gt; exporting layers                                                                                                                  0.6s
 =&gt; =&gt; writing image sha256:b2a6b9cc93dadfb9970a27870b6954baeb46c2af5ac67ee1dd9b304e3db63979                                             0.0s
 =&gt; =&gt; naming to docker.io/library/homehub:test1                                                                                         0.0s
</code></pre></div></div>

<p>I switched the compose file to use my local image</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(venv) (base) builder@LuiGi:~/Workspaces/homehub$ cat compose.yml
services:
  homehub:
    container_name: homehub
    image: homehub:test1
    # build: .
    ports:
      - "5000:5000"
    volumes:
      - ./uploads:/app/uploads
      - ./media:/app/media
      - ./pdfs:/app/pdfs
      - ./data:/app/data
      - ./config.yml:/app/config.yml:ro
    environment:
      - FLASK_ENV=production
      - SECRET_KEY=${SECRET_KEY:-}
</code></pre></div></div>

<p>Then I’ll add passwords for the users.  I’m not sure if I also need the family members so I’ll leave them for the moment in the config.yaml</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>...snip...
users:
  - name: "Mom"
    password: "mompassword"
  - name: "Dad"
    password: "dadpassword"
  - name: "Administrator"
    password: "adminpassword"
  - name: "Freddy"
    password: "d0ntsl33p"
  - name: "Chucky"
    password: "l3tsPl4y"
  - name: "Jason"
    password: "H4ppyH0llow33n"

family_members:
  - Mom
  - Dad
  - Chucky
  - Jason
  - Freddy
... snip ...
</code></pre></div></div>

<p>Now I can see it needs a login</p>

<p><a href="/content/images/2026/03/homehub-11.png"><img src="/content/images/2026/03/homehub-11.png" alt="/content/images/2026/03/homehub-11.png" /></a></p>

<p>and if I enter the right password, I can join</p>

<p><a href="/content/images/2026/03/homehub-12.png"><img src="/content/images/2026/03/homehub-12.png" alt="/content/images/2026/03/homehub-12.png" /></a></p>

<p>I can, of course, log out and login as a different user now</p>

<p><a href="/content/images/2026/03/homehub-13.png"><img src="/content/images/2026/03/homehub-13.png" alt="/content/images/2026/03/homehub-13.png" /></a></p>

<p>And we can see the logged in user and logout buttons in the top right</p>

<p><a href="/content/images/2026/03/homehub-14.png"><img src="/content/images/2026/03/homehub-14.png" alt="/content/images/2026/03/homehub-14.png" /></a></p>

<p>I’m not sure if the author would want AI driven updates. But in case he doesn’t, I did stuff this container up into Dockerhub in case any of y’all want a go with it</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(venv) (base) builder@LuiGi:~/Workspaces/homehub$ docker tag homehub:test1 idjohnson/homehub:test1
(venv) (base) builder@LuiGi:~/Workspaces/homehub$ docker push idjohnson/homehub:test1
The push refers to repository [docker.io/idjohnson/homehub]
adec628c24d1: Pushed
932acd45322f: Pushed
7c35b30fc0bf: Pushed
e02e32e048a9: Pushed
c2a60e60649b: Pushed
f8156944f2a6: Pushed
f045c92e01b4: Mounted from library/python
60285744fe26: Mounted from library/python
2a320211b927: Mounted from library/python
989e799e6349: Mounted from idjohnson/toolstack
test1: digest: sha256:d4d7cdca9e035712ce3995a45449538cc8d3c17c8818496cd0b5c1d0efa8d843 size: 2413
</code></pre></div></div>

<h1 id="summary">Summary</h1>

<p>Following a <a href="https://mariushosting.com/how-to-install-homehub-on-your-synology-nas/">Marius post</a> we looked at <a href="https://github.com/surajverma/homehub">Homehub</a> and <a href="https://github.com/surajverma/toolstack">Toolstack</a>, both created by <a href="https://www.surajverma.in/">Suraj</a>.</p>

<p>In the case of Homehub, we used Gemini CLI to enhance it to have login and passwords and shared it on Dockerhub for others.  With Toolstack, we made a simple Dockerfile and shared it on Kubernetes.</p>

<p>I like Toolstack - it’s pretty useful.  I’ll likely keep that <a href="https://toolbox.tpk.pw">https://toolbox.tpk.pw</a> instance running as it’s like a low cost swiss army knife.</p>

<p>The author, <a href="https://www.surajverma.in/about/">Suraj Verma</a> has worked at a lot of eCommerce sites.  I’m guessing the public Github repos are just for him to work ideas out as he mentions he’s worked on 50+ websites.  My guess is he is a go-getter who does OS stuff on the side between gigs.  Either way, good stuff and I’ll likely be back to see what he has in the future.</p>]]></content><author><name>Isaac Johnson</name></author><category term="Gemini" /><category term="Homehub" /><category term="Toolstack" /><category term="Opensource" /><category term="containers" /><category term="kubernetes" /><summary type="html"><![CDATA[I had bookmarked this Marius post on Homehub, a nice family hub app. I wanted to check that out and maybe see if I could tweak it a bit.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://freshbrewed.science/content/images/2026/03/homehubbg.png" /><media:content medium="image" url="https://freshbrewed.science/content/images/2026/03/homehubbg.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>