1
publish helm chart
Justin Klein edited this page 2026-10-01 22:43:52 +00:00

publish-helm-chart

.gitea/workflows/publish-helm-chart.yml — packt ein Helm-Chart und lädt es in eine Helm-Registry. Die Eingabe-Tabelle, das Aufrufbeispiel und die Voraussetzungen stehen im README; diese Seite begründet den Baustein, und vor allem den Upload-Schritt. Jede seiner Eigenschaften ist einmal bezahlt worden.

Der Baustein kommt aus jlab-cluster/helm-charts, wo er die dünnen publish-<chart>.yml-Hüllen bedient, und ist mit dem Umzug hierher an zwei Stellen angepasst worden — siehe „Der Eigentümer ist eine Eingabe" und „Der Connect-Host ist eine Variable des Aufrufers" weiter unten.

Die Registry ist klassisch, nicht OCI

Das Ziel ist eine indexbasierte Helm-Registry. Es gibt dort keine Registry-Sitzung, also kein helm registry login und kein helm push: Der Upload ist ein einzelnes HTTP-POST des gepackten .tgz, je Anfrage authentifiziert. Das ist der Grund, warum der Schritt curl benutzt und nicht helm — und damit der Grund für fast alles, was danach kommt. helm push würde einen Fehler von selbst zum Fehler machen; curl tut das nicht.

Die Ziel-URL wird zusammengesetzt, nicht gespeichert

<charts-url>/<registry-owner>/helm/api/charts

<charts-url> liegt im Tresor und enthält nur die Basis bis /api/packages. Der Eigentümer kommt als Eingabe, Pakettyp und Endpunkt setzt der Workflow selbst an.

Der Gewinn: Im aufrufenden Repository steht kein Wert, der auf die Registry zeigt — nicht einmal ein Pfadsegment. Und ein zweiter Eigentümer auf derselben Instanz braucht keine zweite Einstellung.

Der Preis: Eine falsche Form des Tresor-Felds ergibt eine falsche URL, und die URL ist im Protokoll maskiert. Ein 404 dagegen wäre nicht zu deuten. Deshalb weisen drei Wächter eine andere Form namentlich zurück und sagen, was im Tresor-Eintrag zu ändern ist:

  • der Wert endet auf /api/charts — das ist die alte, vollständige Form;
  • der Wert enthält das Segment /helm — ebenfalls die alte Form;
  • der Wert endet nicht auf /api/packages — irgendetwas anderes.

Der Eigentümer ist eine Eingabe

registry-owner ist Pflicht und hat keine Vorgabe. Das folgt der Hausregel von Home — ein Eigentümer-Segment wird nie geraten — und ersetzt die frühere Ableitung aus dem Eigentümer des laufenden Repositories.

Die Ableitung war im Herkunfts-Repository richtig, dort ist der Wert gemessen. Dass der Kontext auch in einem von fremd aufgerufenen Workflow der des Aufrufers bleibt, ist dokumentiert, auf dieser Forge-Fassung aber nicht gemessen. Als Eingabe wird der Wert dort ausgewertet, wo er bekannt ist, und der Baustein hängt an keiner Kontext-Semantik — auch nicht an einer zugesicherten. Ein Chart, das unter dem falschen Eigentümer veröffentlicht wird, ist nichts, wofür man sich darauf verlassen müsste.

Ein zweiter Wächter prüft den Wert auf URL-taugliche Zeichen. Ein leerer Eigentümer ergäbe <basis>//helm/api/charts, ein / oder ? darin etwas völlig anderes — beides sind nackte 404 gegen eine maskierte Adresse.

Alle Eingaben des Bausteins sind type: string, auch wenn künftig ein Schalter hinzukommt. Warum: Gemessenes-Verhalten.

Der Connect-Host ist eine Variable des Aufrufers

Im Herkunfts-Repository war der 1Password-Connect-Host fest verdrahtet. Hier ist er eine Variable des aufrufenden Repositories — ein geteilter Baustein trägt nicht die Infrastruktur-Adresse eines einzelnen Mandanten.

Der Preis ist eine Voraussetzung, die man vergessen kann, und eine nicht gesetzte Variable kommt als leere Zeichenkette an. Ein leerer Host scheitert nicht dort, wo er gesetzt wird, sondern zwei Schritte später in der Lade-Action, gegen eine Adresse, die niemand sieht. Deshalb prüft der erste Schritt des Jobs die Variable und benennt beim Abbruch die Einstellung, den Ort und die erwartete Form.

Die Abhängigkeiten gehören dem Aufrufer

ci/helm-dependencies.sh ist ein Skript des aufrufenden Repositories, und das bleibt nach dem Umzug so: Der Checkout holt den Baum des Repositories, zu dem der Lauf gehört — den des Aufrufers —, also ist der Arbeitsbereich dessen Baum und das Skript dessen Kopie. Es ist damit eine Voraussetzung jedes Aufrufers, genauso wie der Connect-Host, und ein Wächter sagt das beim Fehlen, statt die Shell eine fehlende Datei melden zu lassen.

Der Aufruf steht vor helm package, weil helm package mit found in Chart.yaml, but missing in charts/ directory abbricht, solange eine deklarierte Abhängigkeit nicht geholt ist. Für ein Chart ohne dependencies: tut das Skript nichts.

Eine Folge für das veröffentlichte Artefakt, die man kennen muss: Das .tgz des Subcharts wird in unseres gepackt. Die in Chart.yaml gepinnte Version ist damit die Version, die Betreiber bekommen, und nichts stromabwärts sieht die Abhängigkeit jemals wieder — ein Sicherheitsupdate des Subcharts erreicht einen Cluster erst, wenn das äußere Chart neu veröffentlicht wird.

Der Upload-Schritt, Eigenschaft für Eigenschaft

Dies ist der Kern der Seite. Jeder Punkt ist einmal teuer gewesen; wer einen davon als Umstand entfernt, holt den jeweiligen Fehler zurück.

--fail-with-body, sonst ist ein 401 ein Erfolg

curl verlässt einen HTTP-Fehler mit Code 0, solange man nichts anderes verlangt. Ohne dieses Flag wäre die Veröffentlichung also grün, während nichts hochgeladen wurde. Gemessen gegen Gitea 1.24.7: Ein 401 ohne das Flag ist Exit 0, mit dem Flag Exit 22 — und der Antwortkörper wird weiterhin ausgegeben, sodass das Protokoll die Ursache nennt.

Das ist der wichtigste Unterschied zu helm push, das von selbst scheitert.

curl --config - statt --user

curl --user $U:$P schreibt die Zugangsdaten in argv, wo sie in der Prozessliste stehen. curl --config - liest seine Optionen stattdessen von der Standardeingabe; nichts Sensibles erscheint in irgendeinem Argument.

Der Preis dieser Konfigurationsdatei ist ihre Wertsyntax: Ein unquotierter Wert endet am ersten Leerzeichen. Gemessen an curl 8.5.0 — aus has space kam has an. Ein Leerzeichen im Passwort käme deshalb als rätselhafter 401 zurück. Der Schritt weist Leerzeichen in den Zugangsdaten vorher ab und sagt warum. Ein Zugriffstoken dieser Forge ist 40 Hexzeichen lang, der Wächter greift also nur bei einem falsch eingetragenen Tresor-Eintrag.

Der Dateiname wird gebaut, nicht geglobt

--upload-file nimmt genau eine Datei. Ein Glob, der zwei trifft, macht die zweite zum URL-Argument von curl; ein Glob, der keine trifft, wird wörtlich durchgereicht. Beides scheitert unverständlich.

Der Name wird deshalb aus Chart-Name und gelesener Version zusammengesetzt, und ein test -f prüft ihn vorher — beim Fehlschlag listet die Meldung auf, was helm package tatsächlich erzeugt hat. Dass das zusammenpasst, ist gemessen: helm package behält das + einer Version der Form 1.4.0+up1.9.2 wörtlich im Dateinamen, <name>-<version>.tgz ist also eine exakte Entsprechung.

Die Werte stehen im env des Schritts, nicht in der Kommandozeile

Sie werden als quotierte Shell-Variablen gelesen und nie in die Kommandozeile interpoliert. Jeder Wert hat einen :?-Wächter, der beim Fehlen das fehlende op://-Feld benennt.

Warum das nötig wurde: Mit dem Sprung auf die neue Hauptfassung der 1Password-Lade-Action exportiert sie die geladenen Werte nicht mehr als Umgebungsvariablen für Folgeschritte, wie die Vorfassung es tat; nur noch die Step-Outputs tragen sie. Jeder Verweis über die Umgebung löste von diesem Sprung an zu einer leeren Zeichenkette auf — und nichts meldete einen Fehler: Der Ladeschritt protokolliert „Populating variable" und bleibt so oder so grün. Die Wächter scheitern laut, und das Quoten verhindert, dass ein leerer Wert stillschweigend die Argumente verschiebt.

Der Host wird zusätzlich maskiert

1Password maskiert die vollständige URL im Protokoll. Ein Verbindungsfehler von curl gibt aber nur <host>:<port> aus, und das deckt diese Maske nicht. Der Schritt leitet den Host aus der rohen URL ab und maskiert ihn selbst, bevor sonst etwas läuft.

Was der Baustein nicht tut

Er löst sich nicht selbst aus und hat keinen workflow_dispatch. Ein Dispatch hier liefe gegen dieses Repository, in dem kein Chart liegt. Wann veröffentlicht wird, entscheidet die Hülle im aufrufenden Repository über ihr eigenes on: — üblicherweise ein Push auf den Hauptzweig, eingeschränkt auf das Chart-Verzeichnis, dazu ein workflow_dispatch für den Fall, dass von Hand nachgeschoben werden muss.

Er prüft das Chart auch nicht. Lint, Render und Install-Test sind Sache des aufrufenden Repositories, weil sie an dessen Fixtures hängen. Ein Baustein, den nur ein Repository benutzen kann, wäre nicht geteilt, sondern verlagert.