feat(update): Testing/Stable-Update-Kanäle wie enconf (Suite=trixie, Komponente=Kanal)
Installer (EDGEGUARD_CHANNEL), Kanal-Lesen/-Schreiben ohne DB-State (sources.list ist Quelle der Wahrheit), Cluster-Endpoints für Kanalwechsel mit mTLS-Peer-Propagation + Drift-Erkennung, --allow-downgrades für testing→stable-Downgrades über den bestehenden sicheren Rolling-Update- Flow, Settings-UI mit Bestätigung, neues scripts/release.sh (Testing-Push datumsbasiert YYYY.MM.DD.NN, Stable-Promotion mit Verify-Gate + Git-Tag), publish.sh/cleanup-old.sh kanalfähig mit Stable-Tag-Schutz. Migriert Bestandsnodes automatisch von der alten "main"-Komponente auf "stable" (postinst, idempotent) — ohne das würden vor diesem Release installierte Nodes stillschweigend keine Updates mehr sehen, sobald main nicht mehr bespielt wird. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
13
Makefile
13
Makefile
@@ -114,17 +114,18 @@ deb-arm64: release-check build-linux-arm64 ui
|
|||||||
|
|
||||||
deb: deb-amd64 deb-arm64
|
deb: deb-amd64 deb-arm64
|
||||||
|
|
||||||
GITEA_DEB_URL := https://git.netcell-it.de/api/packages/projekte/debian/pool/trixie/main/upload
|
# Direktes `make publish` bleibt als Handnotbremse erhalten, veröffentlicht
|
||||||
|
# aber immer nach stable — für Testing-Releases + das Stable-Promotion-
|
||||||
|
# Gate (verify_channel_debs, Version-Bump, Git-Tag) scripts/release.sh nutzen.
|
||||||
publish-amd64: deb-amd64
|
publish-amd64: deb-amd64
|
||||||
@./scripts/apt-repo/publish.sh $(VERSION) amd64
|
@./scripts/apt-repo/publish.sh $(VERSION) amd64 stable
|
||||||
@echo " -> cleanup-old (keep last $${KEEP:-10})"
|
@echo " -> cleanup-old (keep last $${KEEP:-10})"
|
||||||
@./scripts/apt-repo/cleanup-old.sh
|
@./scripts/apt-repo/cleanup-old.sh stable
|
||||||
|
|
||||||
publish-arm64: deb-arm64
|
publish-arm64: deb-arm64
|
||||||
@./scripts/apt-repo/publish.sh $(VERSION) arm64
|
@./scripts/apt-repo/publish.sh $(VERSION) arm64 stable
|
||||||
@echo " -> cleanup-old (keep last $${KEEP:-10})"
|
@echo " -> cleanup-old (keep last $${KEEP:-10})"
|
||||||
@./scripts/apt-repo/cleanup-old.sh
|
@./scripts/apt-repo/cleanup-old.sh stable
|
||||||
|
|
||||||
publish: publish-amd64 publish-arm64
|
publish: publish-amd64 publish-arm64
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"git.netcell-it.de/projekte/edgeguard-native/internal/cluster/jointoken"
|
"git.netcell-it.de/projekte/edgeguard-native/internal/cluster/jointoken"
|
||||||
"git.netcell-it.de/projekte/edgeguard-native/internal/handlers/response"
|
"git.netcell-it.de/projekte/edgeguard-native/internal/handlers/response"
|
||||||
"git.netcell-it.de/projekte/edgeguard-native/internal/models"
|
"git.netcell-it.de/projekte/edgeguard-native/internal/models"
|
||||||
|
aptsvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/apt"
|
||||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/audit"
|
"git.netcell-it.de/projekte/edgeguard-native/internal/services/audit"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -92,6 +93,8 @@ func (h *ClusterHandler) Register(rg *gin.RouterGroup) {
|
|||||||
g.GET("/repair-replication/status", h.RepairReplicationStatus)
|
g.GET("/repair-replication/status", h.RepairReplicationStatus)
|
||||||
g.GET("/vip-status", h.VIPStatus)
|
g.GET("/vip-status", h.VIPStatus)
|
||||||
g.POST("/vip-test", h.VIPTest)
|
g.POST("/vip-test", h.VIPTest)
|
||||||
|
g.GET("/update-channel", h.UpdateChannel)
|
||||||
|
g.POST("/update-channel", h.SetUpdateChannel)
|
||||||
if h.TLSStore != nil {
|
if h.TLSStore != nil {
|
||||||
g.GET("/cert-status", h.CertStatus)
|
g.GET("/cert-status", h.CertStatus)
|
||||||
g.POST("/renew-self", h.RenewSelf)
|
g.POST("/renew-self", h.RenewSelf)
|
||||||
@@ -247,6 +250,8 @@ func (h *ClusterHandler) RegisterAgent(rg *gin.RouterGroup) {
|
|||||||
g.GET("/master-key", h.AgentMasterKey)
|
g.GET("/master-key", h.AgentMasterKey)
|
||||||
g.GET("/version", h.AgentVersion)
|
g.GET("/version", h.AgentVersion)
|
||||||
g.POST("/trigger-update", h.AgentTriggerUpdate)
|
g.POST("/trigger-update", h.AgentTriggerUpdate)
|
||||||
|
g.POST("/set-channel", h.AgentSetChannel)
|
||||||
|
g.GET("/channel", h.AgentChannel)
|
||||||
g.GET("/active-ips", h.AgentActiveIPs)
|
g.GET("/active-ips", h.AgentActiveIPs)
|
||||||
g.POST("/vip-cmd", h.AgentVIPCmd)
|
g.POST("/vip-cmd", h.AgentVIPCmd)
|
||||||
g.GET("/tls-certs", h.AgentTLSCerts)
|
g.GET("/tls-certs", h.AgentTLSCerts)
|
||||||
@@ -758,7 +763,10 @@ retry_apt() {
|
|||||||
while [ $attempt -lt $max ]; do
|
while [ $attempt -lt $max ]; do
|
||||||
attempt=$((attempt + 1))
|
attempt=$((attempt + 1))
|
||||||
apt-get update -qq || true
|
apt-get update -qq || true
|
||||||
if apt-get install -y -qq -o Dpkg::Options::=--force-confold \
|
# --allow-downgrades: nur relevant nach testing→stable-Kanalwechsel
|
||||||
|
# (Testing-Versionen sortieren datumsbasiert höher als Stable-Semver).
|
||||||
|
# No-Op im Normalfall, da die Candidate sonst immer >= installed ist.
|
||||||
|
if apt-get install -y -qq --allow-downgrades -o Dpkg::Options::=--force-confold \
|
||||||
edgeguard-api edgeguard-ui edgeguard; then return 0; fi
|
edgeguard-api edgeguard-ui edgeguard; then return 0; fi
|
||||||
[ $attempt -lt $max ] && sleep $wait_for && wait_for=$((wait_for * 2))
|
[ $attempt -lt $max ] && sleep $wait_for && wait_for=$((wait_for * 2))
|
||||||
done
|
done
|
||||||
@@ -789,6 +797,131 @@ rm -f /var/lib/edgeguard/upgrade.sh
|
|||||||
c.JSON(http.StatusAccepted, gin.H{"status": "upgrading"})
|
c.JSON(http.StatusAccepted, gin.H{"status": "upgrading"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Update-Kanal (stable/testing) ──────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Kanal-Modell wie enconf (Suite=Codename, Komponente=Kanal, siehe
|
||||||
|
// internal/services/apt.Channel/SetChannel) — an EdgeGuards fixes
|
||||||
|
// Primary/Standby-Paar angepasst statt generischer Server-Flotte: der
|
||||||
|
// Kanal wird auf beiden Nodes synchron gehalten (wie config_hash),
|
||||||
|
// kein Node-Override. Reines Umschreiben der sources.list + `apt-get
|
||||||
|
// update` ist risikofrei (kein Service-Restart, keine VIP-Auswirkung)
|
||||||
|
// — das eigentliche Downgrade/Upgrade auf die neue Kanal-Version läuft
|
||||||
|
// danach ganz normal über den bestehenden (sicheren, Standby-zuerst)
|
||||||
|
// Rolling-Update-Flow, der --allow-downgrades jetzt mit unterstützt.
|
||||||
|
|
||||||
|
type updateChannelResponse struct {
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
PeerChannel string `json:"peer_channel,omitempty"`
|
||||||
|
PeerReached bool `json:"peer_reached"`
|
||||||
|
PeerDrifted bool `json:"peer_drifted"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateChannel liefert den lokalen Kanal + (falls Cluster) den Kanal
|
||||||
|
// des Peers zur Drift-Erkennung — analog zum config_hash-Vergleich.
|
||||||
|
func (h *ClusterHandler) UpdateChannel(c *gin.Context) {
|
||||||
|
resp := updateChannelResponse{Channel: aptsvc.Channel()}
|
||||||
|
peer := h.peerNode(c.Request.Context())
|
||||||
|
if peer != nil && h.Aggregator != nil {
|
||||||
|
results := h.Aggregator.FanOut(c.Request.Context(), []models.HANode{*peer}, "/agent/cluster/channel", h.LocalID)
|
||||||
|
if len(results) > 0 && results[0].OK {
|
||||||
|
var body struct {
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(results[0].Data, &body) == nil {
|
||||||
|
resp.PeerReached = true
|
||||||
|
resp.PeerChannel = body.Channel
|
||||||
|
resp.PeerDrifted = body.Channel != resp.Channel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response.OK(c, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUpdateChannel setzt den Kanal lokal und — falls ein Peer existiert
|
||||||
|
// — synchron auch auf dem Peer via mTLS. Löst KEIN Paket-Update aus;
|
||||||
|
// das übernimmt der Admin danach ganz normal über den Update-Banner /
|
||||||
|
// Rolling-Update, der die neue Candidate-Version dann bereits sieht.
|
||||||
|
func (h *ClusterHandler) SetUpdateChannel(c *gin.Context) {
|
||||||
|
var req struct {
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Channel != "stable" && req.Channel != "testing" {
|
||||||
|
response.BadRequest(c, fmt.Errorf("channel must be 'stable' or 'testing'"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := aptsvc.SetChannel(c.Request.Context(), req.Channel); err != nil {
|
||||||
|
response.Internal(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := updateChannelResponse{Channel: req.Channel}
|
||||||
|
if peer := h.peerNode(c.Request.Context()); peer != nil && h.Aggregator != nil {
|
||||||
|
body, _ := json.Marshal(req)
|
||||||
|
result := h.Aggregator.PostPeerWithBody(c.Request.Context(), *peer, "/agent/cluster/set-channel", body)
|
||||||
|
resp.PeerReached = result.OK
|
||||||
|
if !result.OK {
|
||||||
|
slog.Warn("cluster: set-channel on peer failed", "peer", peer.FQDN, "error", result.Err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.Audit != nil {
|
||||||
|
_ = h.Audit.Log(c.Request.Context(), actorOf(c), "system.update_channel.set",
|
||||||
|
"", gin.H{"channel": req.Channel}, h.NodeID)
|
||||||
|
}
|
||||||
|
response.OK(c, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AgentChannel: mTLS-Peer-Read des lokalen Kanals (für Drift-Anzeige).
|
||||||
|
func (h *ClusterHandler) AgentChannel(c *gin.Context) {
|
||||||
|
response.OK(c, gin.H{"channel": aptsvc.Channel()})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AgentSetChannel: mTLS-Peer-Write — wird vom Primary aufgerufen um den
|
||||||
|
// Kanal auf diesem (Standby-)Node synchron zu setzen.
|
||||||
|
func (h *ClusterHandler) AgentSetChannel(c *gin.Context) {
|
||||||
|
var req struct {
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Channel != "stable" && req.Channel != "testing" {
|
||||||
|
response.BadRequest(c, fmt.Errorf("channel must be 'stable' or 'testing'"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := aptsvc.SetChannel(c.Request.Context(), req.Channel); err != nil {
|
||||||
|
response.Internal(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
slog.Info("cluster: update channel set on this node by primary mTLS call",
|
||||||
|
"channel", req.Channel, "client", c.ClientIP())
|
||||||
|
response.OK(c, gin.H{"channel": req.Channel})
|
||||||
|
}
|
||||||
|
|
||||||
|
// peerNode liefert die einzige andere ha_nodes-Row (best-effort, nil
|
||||||
|
// wenn Standalone oder Store fehlt) — gleiches Muster wie in
|
||||||
|
// RollingUpdate für die Secondary-Ermittlung.
|
||||||
|
func (h *ClusterHandler) peerNode(ctx context.Context) *models.HANode {
|
||||||
|
if h.Store == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
nodes, err := h.Store.List(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for i := range nodes {
|
||||||
|
if nodes[i].ID != h.LocalID {
|
||||||
|
return &nodes[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var errInvalidJoinRequest = simpleError("missing token or csr")
|
var errInvalidJoinRequest = simpleError("missing token or csr")
|
||||||
|
|
||||||
type simpleError string
|
type simpleError string
|
||||||
|
|||||||
@@ -273,7 +273,10 @@ retry_apt() {
|
|||||||
while [ $attempt -lt $max ]; do
|
while [ $attempt -lt $max ]; do
|
||||||
attempt=$((attempt + 1))
|
attempt=$((attempt + 1))
|
||||||
apt-get update -qq || true
|
apt-get update -qq || true
|
||||||
if apt-get install -y -qq -o Dpkg::Options::=--force-confold \
|
# --allow-downgrades: nur relevant nach testing→stable-Kanalwechsel
|
||||||
|
# (Testing-Versionen sortieren datumsbasiert höher als Stable-Semver).
|
||||||
|
# No-Op im Normalfall, da die Candidate sonst immer >= installed ist.
|
||||||
|
if apt-get install -y -qq --allow-downgrades -o Dpkg::Options::=--force-confold \
|
||||||
edgeguard-api edgeguard-ui edgeguard; then return 0; fi
|
edgeguard-api edgeguard-ui edgeguard; then return 0; fi
|
||||||
[ $attempt -lt $max ] && sleep $wait_for && wait_for=$((wait_for * 2))
|
[ $attempt -lt $max ] && sleep $wait_for && wait_for=$((wait_for * 2))
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -940,7 +940,10 @@ retry_apt() {
|
|||||||
attempt=$((attempt + 1))
|
attempt=$((attempt + 1))
|
||||||
echo "[upgrade] attempt $attempt/$max: apt-get update + install"
|
echo "[upgrade] attempt $attempt/$max: apt-get update + install"
|
||||||
apt-get update -qq || true
|
apt-get update -qq || true
|
||||||
if apt-get install -y -qq -o Dpkg::Options::=--force-confold \
|
# --allow-downgrades: nur relevant nach testing→stable-Kanalwechsel
|
||||||
|
# (Testing-Versionen sortieren datumsbasiert höher als Stable-Semver).
|
||||||
|
# No-Op im Normalfall, da die Candidate sonst immer >= installed ist.
|
||||||
|
if apt-get install -y -qq --allow-downgrades -o Dpkg::Options::=--force-confold \
|
||||||
edgeguard-api edgeguard-ui edgeguard; then
|
edgeguard-api edgeguard-ui edgeguard; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -237,3 +237,61 @@ func AutoUpdateEnabled() bool {
|
|||||||
_, err := os.Stat(AutoUpdateConfPath)
|
_, err := os.Stat(AutoUpdateConfPath)
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Update-Kanal (stable/testing) ──────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Kanal-Modell 1:1 von enconf übernommen: Suite = OS-Codename (trixie),
|
||||||
|
// Komponente = Kanal. Kein eigenes Config-File — die sources.list-Zeile
|
||||||
|
// selbst ist die einzige Quelle der Wahrheit (siehe scripts/install.sh
|
||||||
|
// setup_repo(), das dieselbe Zeile beim Erstinstall schreibt).
|
||||||
|
|
||||||
|
// SourcesListPath: vom Installer angelegte apt-Quelle. Root-owned wie
|
||||||
|
// AutoUpdateConfPath — Schreibzugriff nur via sudo tee (Sudoers-Pin im
|
||||||
|
// postinst).
|
||||||
|
const SourcesListPath = "/etc/apt/sources.list.d/edgeguard.list"
|
||||||
|
|
||||||
|
const sourcesListTemplate = "deb [signed-by=/etc/apt/keyrings/nmg.asc] " +
|
||||||
|
"https://git.netcell-it.de/api/packages/projekte/debian trixie %s\n"
|
||||||
|
|
||||||
|
// Channel liest den aktuell konfigurierten Update-Kanal aus dem letzten
|
||||||
|
// Feld der deb-Zeile. Default "stable" wenn die Datei fehlt oder das
|
||||||
|
// letzte Feld kein bekannter Kanal ist (Fail-safe — nie stillschweigend
|
||||||
|
// "testing" annehmen).
|
||||||
|
func Channel() string {
|
||||||
|
data, err := os.ReadFile(SourcesListPath)
|
||||||
|
if err != nil {
|
||||||
|
return "stable"
|
||||||
|
}
|
||||||
|
for _, raw := range strings.Split(string(data), "\n") {
|
||||||
|
line := strings.TrimSpace(raw)
|
||||||
|
if !strings.HasPrefix(line, "deb ") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch fields[len(fields)-1] {
|
||||||
|
case "stable", "testing":
|
||||||
|
return fields[len(fields)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "stable"
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetChannel schreibt die sources.list-Zeile mit dem neuen Kanal und
|
||||||
|
// refresht den apt-Cache sofort — sonst zeigt der Update-Banner bis zum
|
||||||
|
// nächsten 5-min-Throttle-Fenster noch den alten Kanal-Stand.
|
||||||
|
func SetChannel(ctx context.Context, channel string) error {
|
||||||
|
if channel != "stable" && channel != "testing" {
|
||||||
|
return fmt.Errorf("apt: unknown channel %q (expected stable|testing)", channel)
|
||||||
|
}
|
||||||
|
body := fmt.Sprintf(sourcesListTemplate, channel)
|
||||||
|
cmd := exec.Command("sudo", "-n", "/usr/bin/tee", SourcesListPath)
|
||||||
|
cmd.Stdin = strings.NewReader(body)
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("sudo tee %s: %w: %s", SourcesListPath, err, strings.TrimSpace(string(out)))
|
||||||
|
}
|
||||||
|
RefreshNow(ctx)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -925,6 +925,17 @@
|
|||||||
"autoUpdateHint": "Whitelist umfasst nur edgeguard, edgeguard-api, edgeguard-ui. Andere Pakete bleiben unter manueller Kontrolle. Verlangt unattended-upgrades (Distro-Standard auf Trixie). Conf-File: /etc/apt/apt.conf.d/52edgeguard-auto-updates.",
|
"autoUpdateHint": "Whitelist umfasst nur edgeguard, edgeguard-api, edgeguard-ui. Andere Pakete bleiben unter manueller Kontrolle. Verlangt unattended-upgrades (Distro-Standard auf Trixie). Conf-File: /etc/apt/apt.conf.d/52edgeguard-auto-updates.",
|
||||||
"autoUpdateToggled": "Auto-Update-Einstellung gespeichert.",
|
"autoUpdateToggled": "Auto-Update-Einstellung gespeichert.",
|
||||||
"autoUpdateFailed": "Auto-Update-Toggle fehlgeschlagen",
|
"autoUpdateFailed": "Auto-Update-Toggle fehlgeschlagen",
|
||||||
|
"updateChannelCardTitle": "Update-Kanal",
|
||||||
|
"updateChannelStable": "Stable",
|
||||||
|
"updateChannelTesting": "Testing",
|
||||||
|
"updateChannelApply": "Anwenden",
|
||||||
|
"updateChannelConfirmTitle": "Update-Kanal wechseln?",
|
||||||
|
"updateChannelSwitchTestingWarn": "Testing kann instabile Zwischenstände enthalten. Beide Nodes werden umgestellt.",
|
||||||
|
"updateChannelSwitchStableWarn": "Wechsel zurück nach Stable kann ein Downgrade auf beiden Nodes auslösen (Testing-Versionen sind neuer datiert).",
|
||||||
|
"updateChannelSaved": "Update-Kanal gespeichert (beide Nodes).",
|
||||||
|
"updateChannelFailed": "Update-Kanal-Wechsel fehlgeschlagen",
|
||||||
|
"updateChannelDrift": "Kanal-Drift zum Peer-Node erkannt (Peer: {{peer}}) — beim nächsten Wechsel wird synchronisiert.",
|
||||||
|
"updateChannelHint": "Testing zieht datumsbasierte Zwischenversionen aus dem Testing-Repo, Stable die kuratierten Releases. Kanal gilt für beide HA-Nodes synchron.",
|
||||||
"ipv6CardTitle": "IPv6",
|
"ipv6CardTitle": "IPv6",
|
||||||
"ipv6On": "Aktiviert — HAProxy bindet zusätzlich zu IPv4 auf [::]:80, [::]:443 und [::]:3443.",
|
"ipv6On": "Aktiviert — HAProxy bindet zusätzlich zu IPv4 auf [::]:80, [::]:443 und [::]:3443.",
|
||||||
"ipv6Off": "Deaktiviert — HAProxy lauscht nur auf IPv4.",
|
"ipv6Off": "Deaktiviert — HAProxy lauscht nur auf IPv4.",
|
||||||
|
|||||||
@@ -925,6 +925,17 @@
|
|||||||
"autoUpdateHint": "Whitelist covers edgeguard, edgeguard-api, edgeguard-ui only. Other packages stay under manual control. Requires unattended-upgrades (Trixie distro default). Conf file: /etc/apt/apt.conf.d/52edgeguard-auto-updates.",
|
"autoUpdateHint": "Whitelist covers edgeguard, edgeguard-api, edgeguard-ui only. Other packages stay under manual control. Requires unattended-upgrades (Trixie distro default). Conf file: /etc/apt/apt.conf.d/52edgeguard-auto-updates.",
|
||||||
"autoUpdateToggled": "Auto-update setting saved.",
|
"autoUpdateToggled": "Auto-update setting saved.",
|
||||||
"autoUpdateFailed": "Auto-update toggle failed",
|
"autoUpdateFailed": "Auto-update toggle failed",
|
||||||
|
"updateChannelCardTitle": "Update channel",
|
||||||
|
"updateChannelStable": "Stable",
|
||||||
|
"updateChannelTesting": "Testing",
|
||||||
|
"updateChannelApply": "Apply",
|
||||||
|
"updateChannelConfirmTitle": "Switch update channel?",
|
||||||
|
"updateChannelSwitchTestingWarn": "Testing may contain unstable interim builds. Both nodes will be switched.",
|
||||||
|
"updateChannelSwitchStableWarn": "Switching back to stable may trigger a downgrade on both nodes (testing versions are dated newer).",
|
||||||
|
"updateChannelSaved": "Update channel saved (both nodes).",
|
||||||
|
"updateChannelFailed": "Update channel switch failed",
|
||||||
|
"updateChannelDrift": "Channel drift detected against the peer node (peer: {{peer}}) — will sync on the next switch.",
|
||||||
|
"updateChannelHint": "Testing pulls date-stamped interim builds from the testing repo, stable pulls curated releases. The channel applies to both HA nodes in sync.",
|
||||||
"ipv6CardTitle": "IPv6",
|
"ipv6CardTitle": "IPv6",
|
||||||
"ipv6On": "Enabled — HAProxy binds on [::]:80, [::]:443 and [::]:3443 in addition to IPv4.",
|
"ipv6On": "Enabled — HAProxy binds on [::]:80, [::]:443 and [::]:3443 in addition to IPv4.",
|
||||||
"ipv6Off": "Disabled — HAProxy listens on IPv4 only.",
|
"ipv6Off": "Disabled — HAProxy listens on IPv4 only.",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Alert, Button, Card, Descriptions, Form, Input, InputNumber, Popconfirm, Select, Space, Spin, Switch, Tooltip, Typography, message } from 'antd'
|
import { Alert, Button, Card, Descriptions, Form, Input, InputNumber, Popconfirm, Select, Space, Spin, Switch, Tooltip, Typography, message } from 'antd'
|
||||||
import { ApartmentOutlined, CloudDownloadOutlined, CloudSyncOutlined, CodeOutlined, CopyOutlined, DatabaseOutlined, DownloadOutlined, ExclamationCircleOutlined, FileSearchOutlined, GlobalOutlined, LockOutlined, MailOutlined, ReloadOutlined, SettingOutlined, StopOutlined, ToolOutlined } from '@ant-design/icons'
|
import { ApartmentOutlined, BranchesOutlined, CloudDownloadOutlined, CloudSyncOutlined, CodeOutlined, CopyOutlined, DatabaseOutlined, DownloadOutlined, ExclamationCircleOutlined, FileSearchOutlined, GlobalOutlined, LockOutlined, MailOutlined, ReloadOutlined, SettingOutlined, StopOutlined, ToolOutlined } from '@ant-design/icons'
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -342,6 +342,30 @@ export default function SettingsPage() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const [channelDraft, setChannelDraft] = useState<string | null>(null)
|
||||||
|
const { data: updateChannel } = useQuery({
|
||||||
|
queryKey: ['cluster', 'update-channel'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const r = await apiClient.get('/cluster/update-channel')
|
||||||
|
return isEnvelope(r.data)
|
||||||
|
? (r.data.data as { channel: string; peer_channel?: string; peer_reached: boolean; peer_drifted: boolean })
|
||||||
|
: { channel: 'stable', peer_reached: false, peer_drifted: false }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const setUpdateChannel = useMutation({
|
||||||
|
mutationFn: async (channel: string) => {
|
||||||
|
const r = await apiClient.post('/cluster/update-channel', { channel })
|
||||||
|
return r.data
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
msg.success(t('settings.updateChannelSaved'))
|
||||||
|
void qc.invalidateQueries({ queryKey: ['cluster', 'update-channel'] })
|
||||||
|
void qc.invalidateQueries({ queryKey: ['system', 'package-versions'] })
|
||||||
|
},
|
||||||
|
onError: (e: Error) => msg.error(t('settings.updateChannelFailed') + ': ' + e.message),
|
||||||
|
onSettled: () => setChannelDraft(null),
|
||||||
|
})
|
||||||
|
|
||||||
const { data: ipv6 } = useQuery({
|
const { data: ipv6 } = useQuery({
|
||||||
queryKey: ['system', 'ipv6'],
|
queryKey: ['system', 'ipv6'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@@ -765,6 +789,55 @@ export default function SettingsPage() {
|
|||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
title={<><BranchesOutlined /> {t('settings.updateChannelCardTitle')}</>}
|
||||||
|
className="mb-12"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
||||||
|
<Space>
|
||||||
|
<Select
|
||||||
|
value={channelDraft ?? updateChannel?.channel ?? 'stable'}
|
||||||
|
style={{ width: 160 }}
|
||||||
|
disabled={isViewer}
|
||||||
|
options={[
|
||||||
|
{ value: 'stable', label: t('settings.updateChannelStable') },
|
||||||
|
{ value: 'testing', label: t('settings.updateChannelTesting') },
|
||||||
|
]}
|
||||||
|
onChange={setChannelDraft}
|
||||||
|
/>
|
||||||
|
{channelDraft && channelDraft !== (updateChannel?.channel ?? 'stable') && (
|
||||||
|
<Popconfirm
|
||||||
|
title={t('settings.updateChannelConfirmTitle')}
|
||||||
|
description={
|
||||||
|
channelDraft === 'testing'
|
||||||
|
? t('settings.updateChannelSwitchTestingWarn')
|
||||||
|
: t('settings.updateChannelSwitchStableWarn')
|
||||||
|
}
|
||||||
|
okText={t('settings.updateChannelApply')}
|
||||||
|
cancelText={t('common.cancel')}
|
||||||
|
onConfirm={() => setUpdateChannel.mutate(channelDraft)}
|
||||||
|
onCancel={() => setChannelDraft(null)}
|
||||||
|
>
|
||||||
|
<Button size="small" type="primary" loading={setUpdateChannel.isPending}>
|
||||||
|
{t('settings.updateChannelApply')}
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
{updateChannel?.peer_drifted && (
|
||||||
|
<Alert
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
message={t('settings.updateChannelDrift', { peer: updateChannel?.peer_channel ?? '?' })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
|
{t('settings.updateChannelHint')}
|
||||||
|
</Typography.Text>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
title={<><GlobalOutlined /> {t('settings.ipv6CardTitle')}</>}
|
title={<><GlobalOutlined /> {t('settings.ipv6CardTitle')}</>}
|
||||||
className="mb-12"
|
className="mb-12"
|
||||||
|
|||||||
@@ -152,6 +152,9 @@ edgeguard ALL=(root) NOPASSWD: /usr/bin/apt-get update
|
|||||||
# NICHT beliebig in /etc/apt/apt.conf.d schreiben darf.
|
# NICHT beliebig in /etc/apt/apt.conf.d schreiben darf.
|
||||||
edgeguard ALL=(root) NOPASSWD: /usr/bin/tee /etc/apt/apt.conf.d/52edgeguard-auto-updates
|
edgeguard ALL=(root) NOPASSWD: /usr/bin/tee /etc/apt/apt.conf.d/52edgeguard-auto-updates
|
||||||
edgeguard ALL=(root) NOPASSWD: /bin/rm -f /etc/apt/apt.conf.d/52edgeguard-auto-updates
|
edgeguard ALL=(root) NOPASSWD: /bin/rm -f /etc/apt/apt.conf.d/52edgeguard-auto-updates
|
||||||
|
# Update-Kanal-Switch (Settings → Update-Kanal) schreibt exakt diese
|
||||||
|
# sources.list-Zeile. Gleiches Restrict-Pattern wie oben.
|
||||||
|
edgeguard ALL=(root) NOPASSWD: /usr/bin/tee /etc/apt/sources.list.d/edgeguard.list
|
||||||
# Backup-Pfad: pg_dump als postgres-User. Whitelist exakt mit
|
# Backup-Pfad: pg_dump als postgres-User. Whitelist exakt mit
|
||||||
# --clean --if-exists --no-owner --no-acl + dem festen DB-Namen.
|
# --clean --if-exists --no-owner --no-acl + dem festen DB-Namen.
|
||||||
edgeguard ALL=(postgres) NOPASSWD: /usr/bin/pg_dump --clean --if-exists --no-owner --no-acl edgeguard
|
edgeguard ALL=(postgres) NOPASSWD: /usr/bin/pg_dump --clean --if-exists --no-owner --no-acl edgeguard
|
||||||
@@ -294,6 +297,19 @@ SUDOERS
|
|||||||
|
|
||||||
chmod 0440 /etc/sudoers.d/edgeguard
|
chmod 0440 /etc/sudoers.d/edgeguard
|
||||||
|
|
||||||
|
# ── Migration: alte "main"-Komponente → "stable" (Kanal-Modell) ──
|
||||||
|
# Vor diesem Release schrieb der Installer immer Komponente "main"
|
||||||
|
# in die apt-Quelle. Ab jetzt publiziert publish.sh/release.sh nur
|
||||||
|
# noch nach stable/testing — ohne diese Migration würden
|
||||||
|
# Bestandsnodes stumm keine neuen Updates mehr sehen (main bleibt
|
||||||
|
# auf dem letzten main-Stand stehen). Idempotent: greift nur wenn
|
||||||
|
# die Datei existiert UND noch auf "main" zeigt.
|
||||||
|
EG_SOURCES_LIST=/etc/apt/sources.list.d/edgeguard.list
|
||||||
|
if [ -f "$EG_SOURCES_LIST" ] && grep -q ' trixie main$' "$EG_SOURCES_LIST"; then
|
||||||
|
sed -i 's/ trixie main$/ trixie stable/' "$EG_SOURCES_LIST"
|
||||||
|
apt-get update -qq || true
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Sysctl-Profil für Edge-Gateway (NAT + HAProxy + Forwarding) ──
|
# ── Sysctl-Profil für Edge-Gateway (NAT + HAProxy + Forwarding) ──
|
||||||
# Voraussetzung für NAT/DNAT/Masquerade + sinnvolle Defaults
|
# Voraussetzung für NAT/DNAT/Masquerade + sinnvolle Defaults
|
||||||
# für eine high-throughput Forwarding-Box. Edit nicht von Hand
|
# für eine high-throughput Forwarding-Box. Edit nicht von Hand
|
||||||
|
|||||||
@@ -1,27 +1,49 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Cleanup-old: löscht alle EdgeGuard-Versionen im Gitea Package
|
# Cleanup-old: löscht alle EdgeGuard-Versionen im Gitea Package
|
||||||
# Registry außer den letzten N (default 10).
|
# Registry außer den letzten N (default 10) — pro Kanal (Komponente).
|
||||||
#
|
#
|
||||||
# Wird vom Makefile direkt nach erfolgreichem Upload aufgerufen. Reihen-
|
# Wird vom Makefile/release.sh direkt nach erfolgreichem Upload
|
||||||
# folge ist wichtig: ERST der neue Build hochladen, DANN die ältesten
|
# aufgerufen. Reihenfolge ist wichtig: ERST der neue Build hochladen,
|
||||||
# wegschmeißen — sonst riskieren wir bei Cleanup-vor-Upload eine Lücke.
|
# DANN die ältesten wegschmeißen — sonst riskieren wir bei Cleanup-vor-
|
||||||
|
# Upload eine Lücke.
|
||||||
|
#
|
||||||
|
# Stable-Schutz: Versionen, die als Git-Tag `v<version>` im Repo stehen,
|
||||||
|
# werden im stable-Kanal NIE gelöscht (analog enconf STABLE_PROTECT) —
|
||||||
|
# ein Kunde, der genau diese Version installiert hat, muss sie über
|
||||||
|
# apt-get weiterhin ziehen können.
|
||||||
#
|
#
|
||||||
# Voraussetzungen:
|
# Voraussetzungen:
|
||||||
# - ~/.gitea-token mit write-Package-Scope
|
# - ~/.gitea-token mit write-Package-Scope
|
||||||
# - jq + curl
|
# - jq + curl
|
||||||
#
|
#
|
||||||
# Aufruf:
|
# Aufruf:
|
||||||
# ./cleanup-old.sh # KEEP=10 (default)
|
# ./cleanup-old.sh # Kanal stable, KEEP=10 (default)
|
||||||
# KEEP=20 ./cleanup-old.sh # mehr behalten
|
# ./cleanup-old.sh testing # Kanal testing
|
||||||
|
# KEEP=20 ./cleanup-old.sh stable # mehr behalten
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
KEEP="${KEEP:-10}"
|
KEEP="${KEEP:-10}"
|
||||||
OWNER="projekte"
|
OWNER="projekte"
|
||||||
DIST="trixie"
|
DIST="trixie"
|
||||||
COMPONENT="main"
|
COMPONENT="${1:-stable}"
|
||||||
|
case "$COMPONENT" in stable|testing) ;; *) echo "cleanup-old: unknown channel '$COMPONENT' (expected stable or testing)" >&2; exit 2 ;; esac
|
||||||
BASE="https://git.netcell-it.de"
|
BASE="https://git.netcell-it.de"
|
||||||
|
|
||||||
|
# Geschützte Versionen (nur relevant im stable-Kanal): jeder vorhandene
|
||||||
|
# Git-Tag `v*` im Repo-Root. Leer wenn nicht im Repo ausgeführt oder im
|
||||||
|
# testing-Kanal (dort gibt es keine Tags/keinen Schutz).
|
||||||
|
PROTECTED_VERSIONS=""
|
||||||
|
if [ "$COMPONENT" = "stable" ]; then
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
PROTECTED_VERSIONS="$(git -C "$REPO_ROOT" tag --list 'v*' 2>/dev/null | sed 's/^v//' || true)"
|
||||||
|
fi
|
||||||
|
is_protected() {
|
||||||
|
local v="$1"
|
||||||
|
[ -z "$PROTECTED_VERSIONS" ] && return 1
|
||||||
|
printf '%s\n' "$PROTECTED_VERSIONS" | grep -qx "$v"
|
||||||
|
}
|
||||||
|
|
||||||
if [ -z "${GITEA_TOKEN:-}" ]; then
|
if [ -z "${GITEA_TOKEN:-}" ]; then
|
||||||
if [ -r "$HOME/.gitea-token" ]; then
|
if [ -r "$HOME/.gitea-token" ]; then
|
||||||
GITEA_TOKEN="$(tr -d '\n' < "$HOME/.gitea-token")"
|
GITEA_TOKEN="$(tr -d '\n' < "$HOME/.gitea-token")"
|
||||||
@@ -37,6 +59,7 @@ TOK="$GITEA_TOKEN"
|
|||||||
PKGS=(
|
PKGS=(
|
||||||
"edgeguard:all"
|
"edgeguard:all"
|
||||||
"edgeguard-api:amd64"
|
"edgeguard-api:amd64"
|
||||||
|
"edgeguard-api:arm64"
|
||||||
"edgeguard-ui:all"
|
"edgeguard-ui:all"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -44,27 +67,37 @@ cleanup_pkg() {
|
|||||||
local pkg="$1"
|
local pkg="$1"
|
||||||
local arch="$2"
|
local arch="$2"
|
||||||
|
|
||||||
# Versionen sammeln. Gitea Package-API liefert flache Liste:
|
# Versionen sammeln. Gitea Package-API liefert flache Liste über ALLE
|
||||||
# /api/v1/packages/{owner}?type=debian&q={name}
|
# Kanäle hinweg: /api/v1/packages/{owner}?type=debian&q={name}
|
||||||
# Wir filtern client-seitig auf name (exact-match — Gitea-Query ist
|
# Wir filtern client-seitig auf name (exact-match — Gitea-Query ist
|
||||||
# leider substring-fuzzy) und sort -V (semver) absteigend.
|
# leider substring-fuzzy) UND auf das Versionsformat des aktuellen
|
||||||
|
# Kanals — stable=Semver (X.Y.Z), testing=datumsbasiert (YYYY.MM.DD.NN).
|
||||||
|
# Die beiden Formate überlappen nie, das hält testing- und stable-
|
||||||
|
# Versionen sauber getrennt, obwohl Gitea (Name,Version) global dedupliziert.
|
||||||
local raw
|
local raw
|
||||||
raw="$(curl -fsS -H "Authorization: token $TOK" \
|
raw="$(curl -fsS -H "Authorization: token $TOK" \
|
||||||
"$BASE/api/v1/packages/$OWNER?type=debian&q=$pkg&limit=1000")"
|
"$BASE/api/v1/packages/$OWNER?type=debian&q=$pkg&limit=1000")"
|
||||||
|
|
||||||
|
local ver_pattern
|
||||||
|
if [ "$COMPONENT" = "stable" ]; then
|
||||||
|
ver_pattern='^[0-9]+\.[0-9]+\.[0-9]+$'
|
||||||
|
else
|
||||||
|
ver_pattern='^[0-9]{4}\.[0-9]{2}\.[0-9]{2}\.[0-9]+$'
|
||||||
|
fi
|
||||||
|
|
||||||
local versions
|
local versions
|
||||||
versions="$(printf '%s' "$raw" | jq -r --arg n "$pkg" \
|
versions="$(printf '%s' "$raw" | jq -r --arg n "$pkg" \
|
||||||
'.[] | select(.name==$n) | .version' | sort -V -r | awk '!seen[$0]++')"
|
'.[] | select(.name==$n) | .version' | grep -E "$ver_pattern" | sort -V -r | awk '!seen[$0]++')"
|
||||||
|
|
||||||
if [ -z "$versions" ]; then
|
if [ -z "$versions" ]; then
|
||||||
echo " $pkg ($arch): no versions found, skip"
|
echo " $pkg ($arch): no $COMPONENT versions found, skip"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local count
|
local count
|
||||||
count="$(printf '%s\n' "$versions" | wc -l)"
|
count="$(printf '%s\n' "$versions" | wc -l)"
|
||||||
if [ "$count" -le "$KEEP" ]; then
|
if [ "$count" -le "$KEEP" ]; then
|
||||||
echo " $pkg ($arch): $count versions, ≤ keep=$KEEP, nothing to delete"
|
echo " $pkg ($arch): $count $COMPONENT versions, ≤ keep=$KEEP, nothing to delete"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -72,10 +105,14 @@ cleanup_pkg() {
|
|||||||
to_delete="$(printf '%s\n' "$versions" | tail -n +$((KEEP+1)))"
|
to_delete="$(printf '%s\n' "$versions" | tail -n +$((KEEP+1)))"
|
||||||
|
|
||||||
local kept_count=$((count - $(printf '%s\n' "$to_delete" | wc -l)))
|
local kept_count=$((count - $(printf '%s\n' "$to_delete" | wc -l)))
|
||||||
echo " $pkg ($arch): $count versions total, keeping $kept_count newest, deleting $(printf '%s\n' "$to_delete" | wc -l)"
|
echo " $pkg ($arch): $count $COMPONENT versions total, keeping $kept_count newest, considering $(printf '%s\n' "$to_delete" | wc -l) for deletion"
|
||||||
|
|
||||||
while IFS= read -r v; do
|
while IFS= read -r v; do
|
||||||
[ -z "$v" ] && continue
|
[ -z "$v" ] && continue
|
||||||
|
if is_protected "$v"; then
|
||||||
|
echo " skip $pkg $v $arch (protected — git tag v$v exists)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
# DELETE-Endpoint für Debian-Pakete:
|
# DELETE-Endpoint für Debian-Pakete:
|
||||||
# /api/packages/{owner}/debian/pool/{dist}/{comp}/{name}/{version}/{arch}
|
# /api/packages/{owner}/debian/pool/{dist}/{comp}/{name}/{version}/{arch}
|
||||||
local url="$BASE/api/packages/$OWNER/debian/pool/$DIST/$COMPONENT/$pkg/$v/$arch"
|
local url="$BASE/api/packages/$OWNER/debian/pool/$DIST/$COMPONENT/$pkg/$v/$arch"
|
||||||
|
|||||||
@@ -11,16 +11,22 @@
|
|||||||
# Befund 2026-05-17 nach Gitea-Disk-Full-Vorfall.
|
# Befund 2026-05-17 nach Gitea-Disk-Full-Vorfall.
|
||||||
#
|
#
|
||||||
# Aufruf:
|
# Aufruf:
|
||||||
# ./publish.sh <version> <arch>
|
# ./publish.sh <version> <arch> [channel]
|
||||||
# arch = amd64 | arm64. arm64 publiziert NUR edgeguard-api (das einzige
|
# arch = amd64 | arm64. arm64 publiziert NUR edgeguard-api (das einzige
|
||||||
# arch-spezifische Paket); amd64 publiziert alle drei.
|
# arch-spezifische Paket); amd64 publiziert alle drei.
|
||||||
|
# channel = stable | testing (Default stable). Kanal-Modell wie enconf:
|
||||||
|
# Suite=trixie fest, Komponente=Kanal. Wird von scripts/release.sh
|
||||||
|
# gesetzt — direkter Aufruf (z.B. aus altem `make publish`) bleibt
|
||||||
|
# ohne 3. Arg unverändert auf stable.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
VERSION="${1:?usage: publish.sh <version> <arch>}"
|
VERSION="${1:?usage: publish.sh <version> <arch> [channel]}"
|
||||||
ARCH="${2:?usage: publish.sh <version> <arch>}"
|
ARCH="${2:?usage: publish.sh <version> <arch> [channel]}"
|
||||||
|
CHANNEL="${3:-stable}"
|
||||||
|
case "$CHANNEL" in stable|testing) ;; *) echo "publish: unknown channel '$CHANNEL' (expected stable or testing)" >&2; exit 2 ;; esac
|
||||||
|
|
||||||
BASE="https://git.netcell-it.de/api/packages/projekte/debian/pool/trixie/main/upload"
|
BASE="https://git.netcell-it.de/api/packages/projekte/debian/pool/trixie/${CHANNEL}/upload"
|
||||||
|
|
||||||
if [ -z "${GITEA_TOKEN:-}" ]; then
|
if [ -z "${GITEA_TOKEN:-}" ]; then
|
||||||
if [ -r "$HOME/.gitea-token" ]; then
|
if [ -r "$HOME/.gitea-token" ]; then
|
||||||
@@ -88,4 +94,4 @@ case "$ARCH" in
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
echo "publish: ok ($VERSION/$ARCH)"
|
echo "publish: ok ($VERSION/$ARCH/$CHANNEL)"
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
#
|
#
|
||||||
# curl -fsSL https://get.netcell-edgeguard.de | sudo bash
|
# curl -fsSL https://get.netcell-edgeguard.de | sudo bash
|
||||||
#
|
#
|
||||||
|
# Kanal wählen (Default: stable):
|
||||||
|
# curl -fsSL https://get.netcell-edgeguard.de | EDGEGUARD_CHANNEL=testing sudo -E bash
|
||||||
|
#
|
||||||
# Supported: Debian 13 (Trixie), amd64 + arm64.
|
# Supported: Debian 13 (Trixie), amd64 + arm64.
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
@@ -111,12 +114,14 @@ setup_repo() {
|
|||||||
curl -fsSL "https://git.netcell-it.de/api/packages/projekte/debian/repository.key" \
|
curl -fsSL "https://git.netcell-it.de/api/packages/projekte/debian/repository.key" \
|
||||||
-o /etc/apt/keyrings/nmg.asc
|
-o /etc/apt/keyrings/nmg.asc
|
||||||
fi
|
fi
|
||||||
echo "deb [signed-by=/etc/apt/keyrings/nmg.asc] https://git.netcell-it.de/api/packages/projekte/debian trixie main" \
|
# Kanal-Modell (wie enconf): Suite = OS-Codename, Komponente = Kanal.
|
||||||
|
# Default stable; EDGEGUARD_CHANNEL=testing für Testing-Kanal.
|
||||||
|
echo "deb [signed-by=/etc/apt/keyrings/nmg.asc] https://git.netcell-it.de/api/packages/projekte/debian trixie ${EDGEGUARD_CHANNEL:-stable}" \
|
||||||
> /etc/apt/sources.list.d/edgeguard.list
|
> /etc/apt/sources.list.d/edgeguard.list
|
||||||
|
|
||||||
apt-get update -qq
|
apt-get update -qq
|
||||||
}
|
}
|
||||||
step "Set up EdgeGuard apt repository" setup_repo
|
step "Set up EdgeGuard apt repository (${EDGEGUARD_CHANNEL:-stable})" setup_repo
|
||||||
|
|
||||||
AVAILABLE=$(LC_ALL=C apt-cache policy edgeguard 2>/dev/null | awk '/Candidate:/ {print $2; exit}' || true)
|
AVAILABLE=$(LC_ALL=C apt-cache policy edgeguard 2>/dev/null | awk '/Candidate:/ {print $2; exit}' || true)
|
||||||
if [ -n "$AVAILABLE" ] && [ "$AVAILABLE" != "(none)" ]; then
|
if [ -n "$AVAILABLE" ] && [ "$AVAILABLE" != "(none)" ]; then
|
||||||
|
|||||||
181
scripts/release.sh
Executable file
181
scripts/release.sh
Executable file
@@ -0,0 +1,181 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# EdgeGuard — Release erstellen (Testing-Push oder Stable-Promotion)
|
||||||
|
#
|
||||||
|
# Verwendung:
|
||||||
|
# ./scripts/release.sh # Testing-Push: baut den aktuellen
|
||||||
|
# # Stand, Version = datumsbasiert
|
||||||
|
# # YYYY.MM.DD.NN, Upload → testing
|
||||||
|
# ./scripts/release.sh stable # Stable-Promotion: baut den
|
||||||
|
# # aktuellen Stand unter der
|
||||||
|
# # nächsten Patch-Version (VERSION-
|
||||||
|
# # Datei +1), Upload → stable,
|
||||||
|
# # VERSION-Commit + Git-Tag v<version>
|
||||||
|
# ./scripts/release.sh stable 1.4.0 # Stable mit expliziter Version
|
||||||
|
#
|
||||||
|
# Kanal-Modell 1:1 von enconf (netcell-webpanel) übernommen — Suite=trixie
|
||||||
|
# fest, Komponente=Kanal (stable/testing). An EdgeGuard angepasst:
|
||||||
|
# - Gates laufen über die bestehende Go-Quality-Baseline (`make deb` ruft
|
||||||
|
# `release-check` + `management-ui`-tsc automatisch auf) statt eigener
|
||||||
|
# Preflight-Schritte.
|
||||||
|
# - Kein Docs-/Changelog-/Checksum-/Marketing-Site-Deploy — dafür
|
||||||
|
# existiert bei EdgeGuard keine Infrastruktur (siehe CLAUDE.md).
|
||||||
|
# - Testing-Versionen sind datumsbasiert (wie enconf) DAMIT sie (a) bei
|
||||||
|
# apt immer über jeder Stable-Semver-Version sortieren und (b) nie mit
|
||||||
|
# einer Stable-Versionsnummer kollidieren — Gitea dedupliziert
|
||||||
|
# (Name,Version) global über alle Kanäle hinweg.
|
||||||
|
#
|
||||||
|
# Voraussetzung: GITEA_TOKEN (env oder ~/.gitea-token) mit Package-
|
||||||
|
# Upload/Delete/Query-Scope.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
cd "$REPO_DIR"
|
||||||
|
|
||||||
|
OWNER="projekte"
|
||||||
|
GITEA_URL="https://git.netcell-it.de"
|
||||||
|
KEEP="${KEEP:-10}"
|
||||||
|
|
||||||
|
log() { echo "[release] $*"; }
|
||||||
|
warn() { echo " ⚠ $*" >&2; }
|
||||||
|
abort() { echo "🛑 ABBRUCH: $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
if [ -z "${GITEA_TOKEN:-}" ]; then
|
||||||
|
if [ -r "$HOME/.gitea-token" ]; then
|
||||||
|
GITEA_TOKEN="$(tr -d '\n' < "$HOME/.gitea-token")"
|
||||||
|
else
|
||||||
|
abort "GITEA_TOKEN fehlt (env oder ~/.gitea-token)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
export GITEA_TOKEN
|
||||||
|
|
||||||
|
# ─── Testing-Version: datumsbasiert YYYY.MM.DD.NN ──────────────────────
|
||||||
|
# NN = laufende Nummer für den Tag, ermittelt aus den bereits im Registry
|
||||||
|
# vorhandenen Versionen des Meta-Pakets (edgeguard) mit dem heutigen
|
||||||
|
# Datumspräfix. Erster Release des Tages -> .01.
|
||||||
|
next_testing_version() {
|
||||||
|
local today
|
||||||
|
today="$(date -u +%Y.%m.%d)"
|
||||||
|
local raw
|
||||||
|
raw="$(curl -fsS -H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
"$GITEA_URL/api/v1/packages/$OWNER?type=debian&q=edgeguard&limit=1000" 2>/dev/null || echo '[]')"
|
||||||
|
local last_n
|
||||||
|
last_n="$(printf '%s' "$raw" | jq -r --arg n "edgeguard" --arg pfx "${today}." \
|
||||||
|
'.[] | select(.name==$n) | .version | select(startswith($pfx))' \
|
||||||
|
| sed "s/^${today}\.//" | sort -n | tail -1)"
|
||||||
|
printf '%s.%02d' "$today" "$(( ${last_n:-0} + 1 ))"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Stable-Version: Patch-Bump der VERSION-Datei (Default) ────────────
|
||||||
|
bump_patch() {
|
||||||
|
local major minor patch
|
||||||
|
IFS='.' read -r major minor patch <<< "$1"
|
||||||
|
echo "${major}.${minor}.$((patch + 1))"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Alte Version im Ziel-Kanal ersetzen — Upload derselben Nummer würde
|
||||||
|
# sonst mit 409 den alten Inhalt behalten (Gitea dedupliziert (Name,
|
||||||
|
# Version,Arch,Kanal-Pool) — best-effort, 404 ist der Normalfall). ──────
|
||||||
|
delete_before_upload() {
|
||||||
|
local channel="$1" version="$2" name="$3" arch="$4"
|
||||||
|
local url="$GITEA_URL/api/packages/$OWNER/debian/pool/trixie/$channel/$name/$version/$arch"
|
||||||
|
curl -sS -o /dev/null -w '' -X DELETE -H "Authorization: token $GITEA_TOKEN" "$url" || true
|
||||||
|
}
|
||||||
|
|
||||||
|
replace_in_channel() {
|
||||||
|
local channel="$1" version="$2"
|
||||||
|
delete_before_upload "$channel" "$version" edgeguard-api amd64
|
||||||
|
delete_before_upload "$channel" "$version" edgeguard-api arm64
|
||||||
|
delete_before_upload "$channel" "$version" edgeguard-ui all
|
||||||
|
delete_before_upload "$channel" "$version" edgeguard all
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── verify_channel_debs — Health-Gate wie enconf verify_stable_debs ───
|
||||||
|
# Holt den Packages-Index eines Kanals und prüft für JEDE dort
|
||||||
|
# referenzierte .deb, ob sie unter ihrer Pool-URL wirklich mit HTTP 200
|
||||||
|
# abrufbar ist (nicht nur ob der Index existiert — Gitea regeneriert den
|
||||||
|
# Index nicht immer zuverlässig nach einem Delete).
|
||||||
|
verify_channel_debs() {
|
||||||
|
local channel="$1"
|
||||||
|
log "verify_channel_debs($channel): jede indexierte .deb muss abrufbar sein..."
|
||||||
|
local files f code bad=0 checked=0
|
||||||
|
files="$(curl -sk -H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
"$GITEA_URL/api/packages/$OWNER/debian/dists/trixie/$channel/binary-amd64/Packages" 2>/dev/null \
|
||||||
|
| awk '/^Filename:/{print $2}')"
|
||||||
|
if [ -z "$files" ]; then
|
||||||
|
warn "$channel: Packages-Index leer/nicht erreichbar — übersprungen"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
while IFS= read -r f; do
|
||||||
|
[ -z "$f" ] && continue
|
||||||
|
checked=$((checked + 1))
|
||||||
|
code="$(curl -sk -o /dev/null -w '%{http_code}' \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" "$GITEA_URL/api/packages/$OWNER/debian/$f")"
|
||||||
|
if [ "$code" != "200" ]; then
|
||||||
|
warn "$channel: $f -> HTTP $code"
|
||||||
|
bad=$((bad + 1))
|
||||||
|
fi
|
||||||
|
done <<< "$files"
|
||||||
|
[ "$bad" -eq 0 ] || abort "$channel: $bad von $checked .deb(s) im Index nicht abrufbar — Registry inkonsistent."
|
||||||
|
log "$channel: $checked .deb(s) verifiziert, alle abrufbar."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Testing-Push ───────────────────────────────────────────────────────
|
||||||
|
testing_push() {
|
||||||
|
local new_version
|
||||||
|
new_version="$(next_testing_version)"
|
||||||
|
log "[testing 1/3] Gates + Build $new_version (make deb — release-check, amd64+arm64+ui)..."
|
||||||
|
make VERSION="$new_version" deb
|
||||||
|
|
||||||
|
log "[testing 2/3] Alte Version im testing-Kanal ersetzen + hochladen..."
|
||||||
|
replace_in_channel testing "$new_version"
|
||||||
|
./scripts/apt-repo/publish.sh "$new_version" amd64 testing
|
||||||
|
./scripts/apt-repo/publish.sh "$new_version" arm64 testing
|
||||||
|
|
||||||
|
log "[testing 3/3] Cleanup (keep last $KEEP) + Verify..."
|
||||||
|
KEEP="$KEEP" ./scripts/apt-repo/cleanup-old.sh testing
|
||||||
|
verify_channel_debs testing
|
||||||
|
|
||||||
|
log "✅ Testing-Release $new_version veröffentlicht."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Stable-Promotion ───────────────────────────────────────────────────
|
||||||
|
promote_stable() {
|
||||||
|
local explicit_version="${1:-}"
|
||||||
|
local current_version new_version
|
||||||
|
current_version="$(cat "$REPO_DIR/VERSION")"
|
||||||
|
new_version="${explicit_version:-$(bump_patch "$current_version")}"
|
||||||
|
|
||||||
|
git -C "$REPO_DIR" rev-parse "v$new_version" >/dev/null 2>&1 && \
|
||||||
|
abort "Git-Tag v$new_version existiert bereits."
|
||||||
|
[ -n "$(git -C "$REPO_DIR" status --porcelain)" ] && \
|
||||||
|
abort "Working Tree nicht sauber — commit/stash erst, dann Stable-Release."
|
||||||
|
|
||||||
|
log "[stable 1/4] Gates + Build $new_version (make deb — release-check, amd64+arm64+ui)..."
|
||||||
|
make VERSION="$new_version" deb
|
||||||
|
|
||||||
|
log "[stable 2/4] Alte Version im stable-Kanal ersetzen + hochladen..."
|
||||||
|
replace_in_channel stable "$new_version"
|
||||||
|
./scripts/apt-repo/publish.sh "$new_version" amd64 stable
|
||||||
|
./scripts/apt-repo/publish.sh "$new_version" arm64 stable
|
||||||
|
|
||||||
|
log "[stable 3/4] VERSION-Datei setzen + Commit + Git-Tag..."
|
||||||
|
printf '%s' "$new_version" > "$REPO_DIR/VERSION"
|
||||||
|
git -C "$REPO_DIR" add VERSION
|
||||||
|
git -C "$REPO_DIR" commit -m "chore(release): v$new_version stable"
|
||||||
|
git -C "$REPO_DIR" tag "v$new_version"
|
||||||
|
|
||||||
|
log "[stable 4/4] Cleanup (keep last $KEEP) + Verify..."
|
||||||
|
KEEP="$KEEP" ./scripts/apt-repo/cleanup-old.sh stable
|
||||||
|
verify_channel_debs stable
|
||||||
|
|
||||||
|
log "✅ Stable-Release v$new_version fertig."
|
||||||
|
log " Push nicht vergessen: git push origin main --tags"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
stable) promote_stable "${2:-}" ;;
|
||||||
|
"") testing_push ;;
|
||||||
|
*) abort "unbekanntes Argument '$1' (erwartet: 'stable' oder kein Argument)" ;;
|
||||||
|
esac
|
||||||
Reference in New Issue
Block a user