Compare commits
5 Commits
wave6-fix-
...
bump-boots
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2e8a16308 | ||
|
|
514da0ce25 | ||
|
|
cd3d9c5d8d | ||
|
|
87d7e51377 | ||
|
|
aab4fa0cc5 |
@ -493,7 +493,16 @@ spec:
|
||||
# 2026-05-16 with admin:b0ed216 stuck in ImagePullBackOff)
|
||||
# - PR #1556 adds the billing→notification wire so the voucher
|
||||
# issuance flow emails the recipient (D28 zero-touch contract)
|
||||
version: 1.4.147
|
||||
#
|
||||
# 1.4.148 (D16 + D17 + D27 founder-flagged bug fixes, t139 verify cycle):
|
||||
# - PR #1583: D16 /cloud nodes multi-cluster fan-out + handover
|
||||
# export retry/reorder/auth-bypass (catalyst-api 2ab8a0e)
|
||||
# - PR #1584: D27 catalog fresh-seed Published=true default
|
||||
# (sme services catalog 964dc15)
|
||||
# - PR #1585: D17 /app/$componentId route-collision fix (catalyst-ui 2ab8a0e)
|
||||
# Caught on t136/t138 fresh-prov runs that bootstrap-kit was
|
||||
# still pinned to 1.4.147 → none of the fixes reached the chroot.
|
||||
version: 1.4.148
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: bp-catalyst-platform
|
||||
|
||||
@ -33,6 +33,20 @@ func (h *Handler) SeedIfEmpty(ctx context.Context) {
|
||||
|
||||
slog.Info("seed: catalog is empty, seeding default data")
|
||||
h.seedAllData(ctx)
|
||||
|
||||
// D27 fix (2026-05-17 t138 bug fix): on FRESH seed the rows are inserted
|
||||
// with zero-value Bool fields — Published=false, Deployable=false.
|
||||
// Marketplace storefront filters with ?published=true so it sees [],
|
||||
// and the UI renders a blank app grid. Founder caught on t136:
|
||||
// "https://marketplace.t136.../apps/ — list of applications are blank".
|
||||
//
|
||||
// The migrations that flip these to the expected defaults were ONLY
|
||||
// being called on the "already populated" path. Call them after
|
||||
// seedAllData so a fresh Sovereign + marketplace.enabled=true renders
|
||||
// 27 published+deployable apps out of the box.
|
||||
h.seedSystemApps(ctx)
|
||||
h.migrateAppDeployable(ctx)
|
||||
h.migrateAppPublished(ctx)
|
||||
}
|
||||
|
||||
// seedAllData inserts the complete catalog: apps, industries, plans, addons, bundles.
|
||||
|
||||
@ -426,6 +426,15 @@ func main() {
|
||||
// record claiming a different FQDN is rejected.
|
||||
r.Post("/api/v1/internal/deployments/import", h.HandleDeploymentImport)
|
||||
|
||||
// D16 PR F (2026-05-17 t138 bug fix): mothership POSTs each
|
||||
// secondary-region kubeconfig here at handover. Same auth model as
|
||||
// /api/v1/internal/deployments/import — no operator session exists
|
||||
// on the child yet, validation is by depID+regionKey safe-id regex.
|
||||
// Earlier registration inside the auth group (rg) caused mothership
|
||||
// POSTs to 401, suppressing the D16 fan-out silently. Bytes never
|
||||
// leave the chroot disk or enter logged structs (INVIOLABLE-PRINCIPLES #10).
|
||||
r.Post("/api/v1/sovereign/secondary-kubeconfig", h.HandleSovereignSecondaryKubeconfig)
|
||||
|
||||
// Wire the tenant registry — flat-file store at
|
||||
// CATALYST_DEPLOYMENTS_DIR/-tenant-registry.json. Per ADR-0001 §6
|
||||
// the catalyst-api is the host process for the unified-rbac slice
|
||||
@ -1188,13 +1197,10 @@ func main() {
|
||||
rg.Post("/api/v1/sovereign/parent-domains", h.AddParentDomain)
|
||||
rg.Delete("/api/v1/sovereign/parent-domains/{name}", h.DeleteParentDomain)
|
||||
rg.Get("/api/v1/sovereign/parent-domains/{name}/propagation", h.GetPropagation)
|
||||
// D16 fan-out (gate D16 multi-region dashboard cluster grouping):
|
||||
// mothership POSTs each secondary region's kubeconfig at handover
|
||||
// so the chroot's k8sCache.Factory can register all clusters +
|
||||
// dashboard handler's per-cluster List() fan-out enumerates all
|
||||
// 3 regions' pods (Layer-1=Cluster renders 3 bubbles, not 1).
|
||||
// Handler at handler/sovereign_secondary_kubeconfig.go.
|
||||
rg.Post("/api/v1/sovereign/secondary-kubeconfig", h.HandleSovereignSecondaryKubeconfig)
|
||||
// D16 secondary-kubeconfig moved OUT of auth group in PR F
|
||||
// (2026-05-17). Now at top-level r.Post (alongside
|
||||
// /api/v1/internal/deployments/import) so mothership handover
|
||||
// POSTs aren't 401'd before any operator session exists.
|
||||
})
|
||||
|
||||
log.Info("catalyst api listening", "port", port)
|
||||
|
||||
@ -25,6 +25,17 @@ import (
|
||||
// exportDeploymentToChild ships the deployment record to the child's
|
||||
// catalyst-api. Called as a goroutine from fireHandover so it never
|
||||
// blocks the SSE emit.
|
||||
//
|
||||
// D16 PR E (2026-05-17 t138 bug fix): the child's ingress + cert + gateway
|
||||
// are racing to become reachable from outside in the seconds after handover
|
||||
// fires. The initial POST routinely fails with EOF / connection refused
|
||||
// because Cilium Gateway hasn't programmed the HTTPRoute yet. Earlier
|
||||
// behaviour (no retry, early return) silently lost both the deployment
|
||||
// record AND the secondary-kubeconfig fan-out (the goroutine was guarded
|
||||
// behind the early return). The fix:
|
||||
// - retry deployment-export with exponential backoff (up to ~5 min)
|
||||
// - kick off secondary-kubeconfig export UNCONDITIONALLY at the top, so
|
||||
// a deployment-export failure can't suppress the D16 fan-out
|
||||
func (h *Handler) exportDeploymentToChild(dep *Deployment, fqdn string) {
|
||||
if h.store == nil {
|
||||
h.log.Warn("deployment-export: no store; cannot export record",
|
||||
@ -37,6 +48,11 @@ func (h *Handler) exportDeploymentToChild(dep *Deployment, fqdn string) {
|
||||
depID := dep.ID
|
||||
dep.mu.Unlock()
|
||||
|
||||
// D16 PR E: kick off secondary-kubeconfig fan-out IMMEDIATELY in its
|
||||
// own goroutine. It is independent of the deployment-record export
|
||||
// — it must not be suppressed by an EOF on the deployment POST.
|
||||
go h.exportSecondaryKubeconfigsToChild(dep, fqdn, depID)
|
||||
|
||||
body, err := json.Marshal(rec)
|
||||
if err != nil {
|
||||
h.log.Error("deployment-export: marshal failed",
|
||||
@ -47,56 +63,65 @@ func (h *Handler) exportDeploymentToChild(dep *Deployment, fqdn string) {
|
||||
}
|
||||
|
||||
url := "https://api." + fqdn + "/api/v1/internal/deployments/import"
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
h.log.Error("deployment-export: NewRequest failed",
|
||||
"id", depID,
|
||||
"url", url,
|
||||
"err", err,
|
||||
)
|
||||
return
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec // child's LE cert may be seconds behind handover; operator browsers always see the validated cert
|
||||
},
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
h.log.Error("deployment-export: POST failed",
|
||||
"id", depID,
|
||||
"url", url,
|
||||
"err", err,
|
||||
)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= 400 {
|
||||
h.log.Error("deployment-export: child rejected import",
|
||||
"id", depID,
|
||||
"url", url,
|
||||
"status", resp.StatusCode,
|
||||
)
|
||||
return
|
||||
}
|
||||
h.log.Info("deployment-export: shipped to child",
|
||||
"id", depID,
|
||||
"url", url,
|
||||
"events", len(rec.Events),
|
||||
)
|
||||
|
||||
// D16 PR B (2026-05-17): after the deployment record is shipped,
|
||||
// iterate the secondary regions and POST each region's kubeconfig
|
||||
// to the chroot's POST /api/v1/sovereign/secondary-kubeconfig
|
||||
// endpoint (PR #1579) so the chroot's k8sCache.Factory can register
|
||||
// every cluster + the dashboard handler's per-cluster fan-out
|
||||
// (PR #1580) enumerates pods from all N regions when
|
||||
// group_by=cluster|region. Without this, Layer-1=Cluster renders
|
||||
// 1 bubble instead of N on a multi-region Sovereign.
|
||||
go h.exportSecondaryKubeconfigsToChild(dep, fqdn, depID)
|
||||
// D16 PR E retry: backoff doubles from 5s up to 60s, total budget 5 min.
|
||||
// Most handovers succeed on attempt 2-4 (15-45s after first try).
|
||||
backoff := 5 * time.Second
|
||||
deadline := time.Now().Add(5 * time.Minute)
|
||||
attempt := 0
|
||||
for time.Now().Before(deadline) {
|
||||
attempt++
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
h.log.Error("deployment-export: NewRequest failed",
|
||||
"id", depID, "url", url, "err", err,
|
||||
)
|
||||
return
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
h.log.Warn("deployment-export: POST failed (will retry)",
|
||||
"id", depID, "url", url, "attempt", attempt, "err", err,
|
||||
)
|
||||
time.Sleep(backoff)
|
||||
if backoff < 60*time.Second {
|
||||
backoff *= 2
|
||||
}
|
||||
continue
|
||||
}
|
||||
status := resp.StatusCode
|
||||
resp.Body.Close()
|
||||
if status >= 500 {
|
||||
h.log.Warn("deployment-export: child 5xx (will retry)",
|
||||
"id", depID, "url", url, "attempt", attempt, "status", status,
|
||||
)
|
||||
time.Sleep(backoff)
|
||||
if backoff < 60*time.Second {
|
||||
backoff *= 2
|
||||
}
|
||||
continue
|
||||
}
|
||||
if status >= 400 {
|
||||
h.log.Error("deployment-export: child 4xx (giving up)",
|
||||
"id", depID, "url", url, "attempt", attempt, "status", status,
|
||||
)
|
||||
return
|
||||
}
|
||||
h.log.Info("deployment-export: shipped to child",
|
||||
"id", depID, "url", url, "attempt", attempt, "events", len(rec.Events),
|
||||
)
|
||||
return
|
||||
}
|
||||
h.log.Error("deployment-export: gave up after 5min retries",
|
||||
"id", depID, "url", url, "attempts", attempt,
|
||||
)
|
||||
}
|
||||
|
||||
// exportSecondaryKubeconfigsToChild iterates the deployment's secondary
|
||||
@ -143,31 +168,64 @@ func (h *Handler) exportSecondaryKubeconfigsToChild(dep *Deployment, fqdn, depID
|
||||
"kubeconfigYaml": string(raw),
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
h.log.Error("d16-export: NewRequest failed",
|
||||
"id", depID, "region", regionKey, "err", err,
|
||||
// D16 PR E (2026-05-17): retry with backoff for the same reason as
|
||||
// the deployment-record export — the chroot's Cilium Gateway +
|
||||
// catalyst-api may not be programmed yet at handover-fire+0.
|
||||
// Budget: 5 min, doubling 5s→60s.
|
||||
backoff := 5 * time.Second
|
||||
deadline := time.Now().Add(5 * time.Minute)
|
||||
attempt := 0
|
||||
ok := false
|
||||
for time.Now().Before(deadline) {
|
||||
attempt++
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
h.log.Error("d16-export: NewRequest failed",
|
||||
"id", depID, "region", regionKey, "err", err,
|
||||
)
|
||||
break
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
h.log.Warn("d16-export: POST failed (will retry)",
|
||||
"id", depID, "region", regionKey, "url", url, "attempt", attempt, "err", err,
|
||||
)
|
||||
time.Sleep(backoff)
|
||||
if backoff < 60*time.Second {
|
||||
backoff *= 2
|
||||
}
|
||||
continue
|
||||
}
|
||||
status := resp.StatusCode
|
||||
resp.Body.Close()
|
||||
if status >= 500 {
|
||||
h.log.Warn("d16-export: child 5xx (will retry)",
|
||||
"id", depID, "region", regionKey, "attempt", attempt, "status", status,
|
||||
)
|
||||
time.Sleep(backoff)
|
||||
if backoff < 60*time.Second {
|
||||
backoff *= 2
|
||||
}
|
||||
continue
|
||||
}
|
||||
if status >= 400 {
|
||||
h.log.Error("d16-export: child 4xx (giving up)",
|
||||
"id", depID, "region", regionKey, "attempt", attempt, "status", status,
|
||||
)
|
||||
break
|
||||
}
|
||||
h.log.Info("d16-export: secondary kubeconfig shipped to child",
|
||||
"id", depID, "region", regionKey, "attempt", attempt,
|
||||
)
|
||||
continue
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
h.log.Error("d16-export: POST failed",
|
||||
"id", depID, "region", regionKey, "url", url, "err", err,
|
||||
if !ok {
|
||||
h.log.Error("d16-export: gave up on region",
|
||||
"id", depID, "region", regionKey, "attempts", attempt,
|
||||
)
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode >= 400 {
|
||||
h.log.Error("d16-export: child rejected secondary kubeconfig",
|
||||
"id", depID, "region", regionKey, "status", resp.StatusCode,
|
||||
)
|
||||
continue
|
||||
}
|
||||
h.log.Info("d16-export: secondary kubeconfig shipped to child",
|
||||
"id", depID, "region", regionKey,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -189,14 +247,14 @@ func regionKeysForExport(dep *Deployment) []string {
|
||||
if cr == "" {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, cr+"-"+itoa(i))
|
||||
keys = append(keys, cr+"-"+regionSlotIndex(i))
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// itoa — local int→string without pulling strconv into the import set.
|
||||
// regionSlotIndex — local int→string without pulling strconv into the import set.
|
||||
// Single-digit fast path (we never have >9 regions per Sovereign).
|
||||
func itoa(n int) string {
|
||||
func regionSlotIndex(n int) string {
|
||||
if n >= 0 && n < 10 {
|
||||
return string(rune('0' + n))
|
||||
}
|
||||
|
||||
@ -769,6 +769,15 @@ type sovereignCloudResponse struct {
|
||||
}
|
||||
|
||||
type sovereignNode struct {
|
||||
// Cluster — multi-region fan-out tag (D16 PR D, 2026-05-17). When the
|
||||
// Sovereign Console has secondary kubeconfigs registered with the
|
||||
// chroot's k8sCache (via /api/v1/sovereign/secondary-kubeconfig posted
|
||||
// at handover), HandleSovereignCloud enumerates nodes / LBs / SCs /
|
||||
// PVCs from every registered cluster and tags each row with its
|
||||
// cluster id (e.g., "primary", "nbg1-1", "sin-2") so the operator
|
||||
// can group/filter by region. omitempty for backward compat with
|
||||
// single-cluster Sovereigns.
|
||||
Cluster string `json:"cluster,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
Roles []string `json:"roles"`
|
||||
@ -796,6 +805,7 @@ type sovereignIngress struct {
|
||||
}
|
||||
|
||||
type sovereignLB struct {
|
||||
Cluster string `json:"cluster,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
Type string `json:"type"`
|
||||
@ -805,6 +815,7 @@ type sovereignLB struct {
|
||||
}
|
||||
|
||||
type sovereignSC struct {
|
||||
Cluster string `json:"cluster,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Provisioner string `json:"provisioner"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
@ -812,6 +823,7 @@ type sovereignSC struct {
|
||||
}
|
||||
|
||||
type sovereignPVC struct {
|
||||
Cluster string `json:"cluster,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
StorageClass string `json:"storageClass"`
|
||||
@ -848,11 +860,117 @@ func (h *Handler) HandleSovereignCloud(w http.ResponseWriter, r *http.Request) {
|
||||
PVCs: []sovereignPVC{},
|
||||
}
|
||||
|
||||
if nodes, err := deps.core.CoreV1().Nodes().List(ctx, metav1.ListOptions{}); err == nil {
|
||||
for _, n := range nodes.Items {
|
||||
resp.Nodes = append(resp.Nodes, mapNode(&n))
|
||||
// D16 PR D (2026-05-17): multi-region fan-out. Founder caught on t136
|
||||
// that /cloud?view=list&kind=nodes shows 1 node for a 3-region
|
||||
// Sovereign because this handler was using only the in-cluster
|
||||
// kube client (primary cluster). When secondary kubeconfigs are
|
||||
// registered with h.k8sCache (via the chroot's POST
|
||||
// /api/v1/sovereign/secondary-kubeconfig endpoint and the
|
||||
// mothership handover-export hook), enumerate per-cluster + tag
|
||||
// each row with its cluster id so the UI can group/filter by
|
||||
// region. Single-cluster Sovereigns fall back to the deps client.
|
||||
type clientPair struct {
|
||||
id string
|
||||
core kubernetes.Interface
|
||||
dyn dynamic.Interface
|
||||
}
|
||||
pairs := []clientPair{}
|
||||
if h.k8sCache != nil {
|
||||
for _, cid := range h.k8sCache.Clusters() {
|
||||
cc := h.k8sCache.CoreClient(cid)
|
||||
if cc == nil {
|
||||
continue
|
||||
}
|
||||
dc, _ := h.k8sCache.DynamicClientFor(cid)
|
||||
pairs = append(pairs, clientPair{id: cid, core: cc, dyn: dc})
|
||||
}
|
||||
}
|
||||
if len(pairs) == 0 {
|
||||
// Single-cluster fallback — primary in-cluster client only.
|
||||
pairs = []clientPair{{id: "", core: deps.core, dyn: deps.dyn}}
|
||||
}
|
||||
|
||||
for _, p := range pairs {
|
||||
if nodes, err := p.core.CoreV1().Nodes().List(ctx, metav1.ListOptions{}); err == nil {
|
||||
for _, n := range nodes.Items {
|
||||
row := mapNode(&n)
|
||||
row.Cluster = p.id
|
||||
resp.Nodes = append(resp.Nodes, row)
|
||||
}
|
||||
}
|
||||
if svcs, err := p.core.CoreV1().Services("").List(ctx, metav1.ListOptions{}); err == nil {
|
||||
for _, svc := range svcs.Items {
|
||||
if svc.Spec.Type != corev1.ServiceTypeLoadBalancer {
|
||||
continue
|
||||
}
|
||||
ports := []string{}
|
||||
for _, port := range svc.Spec.Ports {
|
||||
ports = append(ports, fmt.Sprintf("%d/%s", port.Port, port.Protocol))
|
||||
}
|
||||
extIP := ""
|
||||
for _, ing := range svc.Status.LoadBalancer.Ingress {
|
||||
if ing.IP != "" {
|
||||
extIP = ing.IP
|
||||
break
|
||||
}
|
||||
if ing.Hostname != "" {
|
||||
extIP = ing.Hostname
|
||||
break
|
||||
}
|
||||
}
|
||||
resp.LoadBalancers = append(resp.LoadBalancers, sovereignLB{
|
||||
Cluster: p.id,
|
||||
Name: svc.Name,
|
||||
Namespace: svc.Namespace,
|
||||
Type: string(svc.Spec.Type),
|
||||
ClusterIP: svc.Spec.ClusterIP,
|
||||
ExternalIP: extIP,
|
||||
Ports: ports,
|
||||
})
|
||||
}
|
||||
}
|
||||
if scs, err := p.core.StorageV1().StorageClasses().List(ctx, metav1.ListOptions{}); err == nil {
|
||||
for _, sc := range scs.Items {
|
||||
isDefault := sc.Annotations["storageclass.kubernetes.io/is-default-class"] == "true"
|
||||
rp := ""
|
||||
if sc.ReclaimPolicy != nil {
|
||||
rp = string(*sc.ReclaimPolicy)
|
||||
}
|
||||
resp.StorageClasses = append(resp.StorageClasses, sovereignSC{
|
||||
Cluster: p.id,
|
||||
Name: sc.Name,
|
||||
Provisioner: sc.Provisioner,
|
||||
IsDefault: isDefault,
|
||||
ReclaimPolicy: rp,
|
||||
})
|
||||
}
|
||||
}
|
||||
if pvcs, err := p.core.CoreV1().PersistentVolumeClaims("").List(ctx, metav1.ListOptions{}); err == nil {
|
||||
for _, pv := range pvcs.Items {
|
||||
cap := ""
|
||||
if v, ok := pv.Spec.Resources.Requests[corev1.ResourceStorage]; ok {
|
||||
cap = v.String()
|
||||
}
|
||||
sc := ""
|
||||
if pv.Spec.StorageClassName != nil {
|
||||
sc = *pv.Spec.StorageClassName
|
||||
}
|
||||
resp.PVCs = append(resp.PVCs, sovereignPVC{
|
||||
Cluster: p.id,
|
||||
Name: pv.Name,
|
||||
Namespace: pv.Namespace,
|
||||
StorageClass: sc,
|
||||
Capacity: cap,
|
||||
Status: string(pv.Status.Phase),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Namespaces / Ingresses / HTTPRoutes remain primary-only — they are
|
||||
// the operator-facing front-door inventory, served by the primary
|
||||
// (mothership-handed-over) cluster. Per-cluster fan-out would dup
|
||||
// the same logical hostnames across regions.
|
||||
|
||||
if nss, err := deps.core.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}); err == nil {
|
||||
for _, ns := range nss.Items {
|
||||
@ -903,73 +1021,6 @@ func (h *Handler) HandleSovereignCloud(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
if svcs, err := deps.core.CoreV1().Services("").List(ctx, metav1.ListOptions{}); err == nil {
|
||||
for _, svc := range svcs.Items {
|
||||
if svc.Spec.Type != corev1.ServiceTypeLoadBalancer {
|
||||
continue
|
||||
}
|
||||
ports := []string{}
|
||||
for _, p := range svc.Spec.Ports {
|
||||
ports = append(ports, fmt.Sprintf("%d/%s", p.Port, p.Protocol))
|
||||
}
|
||||
extIP := ""
|
||||
for _, ing := range svc.Status.LoadBalancer.Ingress {
|
||||
if ing.IP != "" {
|
||||
extIP = ing.IP
|
||||
break
|
||||
}
|
||||
if ing.Hostname != "" {
|
||||
extIP = ing.Hostname
|
||||
break
|
||||
}
|
||||
}
|
||||
resp.LoadBalancers = append(resp.LoadBalancers, sovereignLB{
|
||||
Name: svc.Name,
|
||||
Namespace: svc.Namespace,
|
||||
Type: string(svc.Spec.Type),
|
||||
ClusterIP: svc.Spec.ClusterIP,
|
||||
ExternalIP: extIP,
|
||||
Ports: ports,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if scs, err := deps.core.StorageV1().StorageClasses().List(ctx, metav1.ListOptions{}); err == nil {
|
||||
for _, sc := range scs.Items {
|
||||
isDefault := sc.Annotations["storageclass.kubernetes.io/is-default-class"] == "true"
|
||||
rp := ""
|
||||
if sc.ReclaimPolicy != nil {
|
||||
rp = string(*sc.ReclaimPolicy)
|
||||
}
|
||||
resp.StorageClasses = append(resp.StorageClasses, sovereignSC{
|
||||
Name: sc.Name,
|
||||
Provisioner: sc.Provisioner,
|
||||
IsDefault: isDefault,
|
||||
ReclaimPolicy: rp,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if pvcs, err := deps.core.CoreV1().PersistentVolumeClaims("").List(ctx, metav1.ListOptions{}); err == nil {
|
||||
for _, p := range pvcs.Items {
|
||||
cap := ""
|
||||
if v, ok := p.Spec.Resources.Requests[corev1.ResourceStorage]; ok {
|
||||
cap = v.String()
|
||||
}
|
||||
sc := ""
|
||||
if p.Spec.StorageClassName != nil {
|
||||
sc = *p.Spec.StorageClassName
|
||||
}
|
||||
resp.PVCs = append(resp.PVCs, sovereignPVC{
|
||||
Name: p.Name,
|
||||
Namespace: p.Namespace,
|
||||
StorageClass: sc,
|
||||
Capacity: cap,
|
||||
Status: string(p.Status.Phase),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
|
||||
@ -1692,52 +1692,80 @@ const routeTree = rootRoute.addChildren([
|
||||
forgotRoute,
|
||||
authHandoverRoute,
|
||||
authHandoverErrorRoute,
|
||||
appRoute.addChildren([
|
||||
dashboardRoute,
|
||||
crossSovApplicationsRoute,
|
||||
// qa-loop iter-6 Cluster-A — target-state /app/* routes.
|
||||
// STATIC paths first so TanStack resolves them before the dynamic
|
||||
// $deploymentId catch-all.
|
||||
appInstallRoute,
|
||||
appInstallBlueprintRoute,
|
||||
appSREComplianceRoute,
|
||||
appSecComplianceRoute,
|
||||
// /app/$deploymentId tree.
|
||||
appDeploymentRoute,
|
||||
appAppsRoute,
|
||||
appAppDetailRoute,
|
||||
appAppDetailTabRoute,
|
||||
appDeploymentInstallRoute,
|
||||
appDeploymentInstallBlueprintRoute,
|
||||
appBlueprintsPublishRoute,
|
||||
appBlueprintsCurateRoute,
|
||||
appUsersListRoute,
|
||||
appUsersNewRoute,
|
||||
appUsersEditRoute,
|
||||
appRBACMultiGrantRoute,
|
||||
appRBACGroupsRoute,
|
||||
appRBACRolesRoute,
|
||||
appRBACMatrixRoute,
|
||||
appRBACAuditRoute,
|
||||
appOrgMembersRoute,
|
||||
appSettingsRoute,
|
||||
appShellsSessionsRoute,
|
||||
appShellsSessionDetailRoute,
|
||||
appNetworkingIndexRoute,
|
||||
appNetworkingRoute,
|
||||
appContinuumListRoute,
|
||||
appContinuumOverviewRoute,
|
||||
appContinuumAuditRoute,
|
||||
appContinuumSettingsRoute,
|
||||
// Resources — static sub-paths first.
|
||||
appResourcesApplyRoute,
|
||||
appResourcesSearchRoute,
|
||||
appResourcesIndexRoute,
|
||||
appResourcesKindRoute,
|
||||
appResourcesKindNsRoute,
|
||||
appPodLogsRoute,
|
||||
appResourceDetailRoute,
|
||||
]),
|
||||
appRoute.addChildren(
|
||||
// D17 PR G (2026-05-17 t136 bug fix): on Sovereign Console
|
||||
// (chroot, console.<sov-fqdn>), the `/app/$deploymentId` dynamic
|
||||
// route under appRoute catches `/app/bp-alloy` BEFORE the chroot's
|
||||
// `consoleAppDetailRoute` at `/app/$componentId` (under
|
||||
// consoleLayoutRoute), because appRoute.addChildren registers
|
||||
// earlier in the rootRoute children. TanStack matches by
|
||||
// declaration order on equally-specific dynamic routes, so the
|
||||
// Sovereign side rendered AppsPage (catalog grid) instead of
|
||||
// AppDetail. Founder caught on t136: "/app/bp-alloy still shows
|
||||
// catalog like view, individual pages are not opening".
|
||||
//
|
||||
// Fix: filter the children list to exclude the mother-only
|
||||
// `/$deploymentId` catch-alls when running on Sovereign mode. The
|
||||
// routes are defined at module load and DETECTED_MODE.mode never
|
||||
// flips during a page lifetime, so this is safe to evaluate once
|
||||
// at routeTree build time.
|
||||
DETECTED_MODE.mode === 'sovereign'
|
||||
? [
|
||||
// Sovereign-mode appRoute children — EXCLUDES every
|
||||
// mother-only `/$deploymentId/*` route so the chroot's
|
||||
// consoleAppDetailRoute at `/app/$componentId` can claim
|
||||
// `/app/bp-alloy` etc. The few mother-only static paths
|
||||
// still listed here are no-ops on Sovereign (the beforeLoad
|
||||
// on each redirects to the per-Sovereign equivalent).
|
||||
dashboardRoute,
|
||||
]
|
||||
: [
|
||||
dashboardRoute,
|
||||
crossSovApplicationsRoute,
|
||||
// qa-loop iter-6 Cluster-A — target-state /app/* routes.
|
||||
// STATIC paths first so TanStack resolves them before the
|
||||
// dynamic $deploymentId catch-all.
|
||||
appInstallRoute,
|
||||
appInstallBlueprintRoute,
|
||||
appSREComplianceRoute,
|
||||
appSecComplianceRoute,
|
||||
// /app/$deploymentId tree.
|
||||
appDeploymentRoute,
|
||||
appAppsRoute,
|
||||
appAppDetailRoute,
|
||||
appAppDetailTabRoute,
|
||||
appDeploymentInstallRoute,
|
||||
appDeploymentInstallBlueprintRoute,
|
||||
appBlueprintsPublishRoute,
|
||||
appBlueprintsCurateRoute,
|
||||
appUsersListRoute,
|
||||
appUsersNewRoute,
|
||||
appUsersEditRoute,
|
||||
appRBACMultiGrantRoute,
|
||||
appRBACGroupsRoute,
|
||||
appRBACRolesRoute,
|
||||
appRBACMatrixRoute,
|
||||
appRBACAuditRoute,
|
||||
appOrgMembersRoute,
|
||||
appSettingsRoute,
|
||||
appShellsSessionsRoute,
|
||||
appShellsSessionDetailRoute,
|
||||
appNetworkingIndexRoute,
|
||||
appNetworkingRoute,
|
||||
appContinuumListRoute,
|
||||
appContinuumOverviewRoute,
|
||||
appContinuumAuditRoute,
|
||||
appContinuumSettingsRoute,
|
||||
// Resources — static sub-paths first.
|
||||
appResourcesApplyRoute,
|
||||
appResourcesSearchRoute,
|
||||
appResourcesIndexRoute,
|
||||
appResourcesKindRoute,
|
||||
appResourcesKindNsRoute,
|
||||
appPodLogsRoute,
|
||||
appResourceDetailRoute,
|
||||
],
|
||||
),
|
||||
wizardLayoutRoute.addChildren([wizardRoute]),
|
||||
successRoute,
|
||||
deploymentsListRoute,
|
||||
|
||||
@ -1058,8 +1058,8 @@ name: bp-catalyst-platform
|
||||
# Fix #154 (HR-timeout audit). Those bumped the HelmRelease
|
||||
# install.timeout. This bumps the chart-INTERNAL wait loop budget
|
||||
# inside the pre-install hook Job, which is a different seam.
|
||||
version: 1.4.147
|
||||
appVersion: 1.4.147
|
||||
version: 1.4.148
|
||||
appVersion: 1.4.148
|
||||
# 1.4.141 (qa-loop Fix #185, prov #38/#39/#41 recurrence — pre-install
|
||||
# hook unscheduable on saturated worker):
|
||||
#
|
||||
|
||||
@ -230,9 +230,9 @@ images:
|
||||
organization: "openova-io/openova"
|
||||
# SHA tags — bump these via CI when building new images.
|
||||
catalystApi:
|
||||
tag: "d92f734"
|
||||
tag: "2ab8a0e"
|
||||
catalystUi:
|
||||
tag: "d92f734"
|
||||
tag: "2ab8a0e"
|
||||
marketplaceApi:
|
||||
tag: "3c2f7e4"
|
||||
console:
|
||||
@ -247,7 +247,7 @@ images:
|
||||
admin:
|
||||
tag: "3c2f7e4"
|
||||
# All 10 SME microservices share one SHA tag (built from the same mono-repo commit).
|
||||
smeTag: "c04b2ec"
|
||||
smeTag: "964dc15"
|
||||
|
||||
# ─── Runtime service coordinates (qa-loop iter-1, cluster
|
||||
# `catalyst-runtime-config-missing`) ────────────────────────────────────
|
||||
|
||||
Loading…
Reference in New Issue
Block a user