Table of Contents
- publish-helm-chart
- Die Registry ist klassisch, nicht OCI
- Die Ziel-URL wird zusammengesetzt, nicht gespeichert
- Der Eigentümer ist eine Eingabe
- Der Connect-Host ist eine Variable des Aufrufers
- Die Abhängigkeiten gehören dem Aufrufer
- Der Upload-Schritt, Eigenschaft für Eigenschaft
- --fail-with-body, sonst ist ein 401 ein Erfolg
- curl --config - statt --user
- Der Dateiname wird gebaut, nicht geglobt
- Die Werte stehen im env des Schritts, nicht in der Kommandozeile
- Der Host wird zusätzlich maskiert
- Was der Baustein nicht tut
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.