From 7221223367b04f5f2430ac164d8e329da68e2a13 Mon Sep 17 00:00:00 2001 From: HananINouman Date: Fri, 24 Jul 2026 19:03:08 +0300 Subject: [PATCH 1/2] feat: publish Obol CLI version for the frontend footer Write obolVersion into obol-stack-config on stack up / tunnel sync and inject OBOL_STACK_VERSION into the frontend pod so the UI can show the running CLI release. Co-authored-by: Cursor --- .../values/obol-frontend.yaml.gotmpl | 6 ++ internal/stack/stack.go | 7 +++ internal/tunnel/tunnel.go | 55 ++++++++++++++++++- 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/internal/embed/infrastructure/values/obol-frontend.yaml.gotmpl b/internal/embed/infrastructure/values/obol-frontend.yaml.gotmpl index 7fa07c00..b9951be7 100644 --- a/internal/embed/infrastructure/values/obol-frontend.yaml.gotmpl +++ b/internal/embed/infrastructure/values/obol-frontend.yaml.gotmpl @@ -23,6 +23,12 @@ image: - name: OBOL_AUTH_DB_PATH value: "/data/auth.sqlite" + # Running Obol CLI version — shown in the local frontend footer. + # Injected by `obol stack up` via OBOL_STACK_VERSION; the frontend also + # reads obolVersion from the obol-stack-config ConfigMap. + - name: OBOL_STACK_VERSION + value: {{ env "OBOL_STACK_VERSION" | default "" | quote }} + # Obol Agent (ADK) in-cluster URL for CopilotKit runtime - name: ADK_AGENT_URL value: "http://obol-agent.agent.svc.cluster.local:8000/" diff --git a/internal/stack/stack.go b/internal/stack/stack.go index 6c1b5464..eb070dd1 100644 --- a/internal/stack/stack.go +++ b/internal/stack/stack.go @@ -32,6 +32,7 @@ import ( "github.com/ObolNetwork/obol-stack/internal/tunnel" "github.com/ObolNetwork/obol-stack/internal/ui" "github.com/ObolNetwork/obol-stack/internal/update" + "github.com/ObolNetwork/obol-stack/internal/version" x402verifier "github.com/ObolNetwork/obol-stack/internal/x402" petname "github.com/dustinkirkland/golang-petname" "gopkg.in/yaml.v3" @@ -488,6 +489,7 @@ func syncDefaults(cfg *config.Config, u *ui.UI, kubeconfigPath string, dataDir s helmfileCmd.Env = append(os.Environ(), "KUBECONFIG="+kubeconfigPath, "STACK_DATA_DIR="+dataDir, + "OBOL_STACK_VERSION="+version.Version, ) // In development mode, build and import local repo images that aren't on a @@ -522,6 +524,11 @@ func syncDefaults(cfg *config.Config, u *ui.UI, kubeconfigPath string, dataDir s u.Success("Default infrastructure deployed") + // Publish the running CLI version for the local frontend footer. + if err := tunnel.SyncStackConfigVersion(cfg); err != nil { + u.Warnf("Could not publish Obol version to frontend config: %v", err) + } + restoredLiteLLMConfig := false if previousLiteLLMConfig != "" { var err error diff --git a/internal/tunnel/tunnel.go b/internal/tunnel/tunnel.go index d20f6553..f26a7ed7 100644 --- a/internal/tunnel/tunnel.go +++ b/internal/tunnel/tunnel.go @@ -20,6 +20,7 @@ import ( "github.com/ObolNetwork/obol-stack/internal/config" "github.com/ObolNetwork/obol-stack/internal/images" "github.com/ObolNetwork/obol-stack/internal/ui" + "github.com/ObolNetwork/obol-stack/internal/version" ) const ( @@ -987,8 +988,9 @@ func freeLocalPort() (int, error) { } // SyncTunnelConfigMap creates or patches the obol-stack-config ConfigMap in the -// obol-frontend namespace with the current tunnel URL. The frontend reads this -// ConfigMap to construct the correct dashboard URL. +// obol-frontend namespace with the current tunnel URL and the running Obol CLI +// version. The frontend reads this ConfigMap for the public tunnel URL and the +// footer version display. func SyncTunnelConfigMap(cfg *config.Config, tunnelURL string) error { kubectlPath := filepath.Join(cfg.BinDir, "kubectl") kubeconfigPath := filepath.Join(cfg.ConfigDir, "kubeconfig.yaml") @@ -1007,7 +1009,8 @@ metadata: namespace: obol-frontend data: tunnelURL: %s -`, strings.TrimRight(tunnelURL, "/")) + obolVersion: %s +`, strings.TrimRight(tunnelURL, "/"), version.Version) // Server-side apply avoids the flaky client-side /openapi/v2 download on k3d. cmd := exec.Command(kubectlPath, @@ -1023,6 +1026,52 @@ data: return nil } +// SyncStackConfigVersion patches obolVersion into obol-frontend/obol-stack-config +// without touching tunnelURL. Used on stack up when no tunnel sync runs yet. +func SyncStackConfigVersion(cfg *config.Config) error { + kubectlPath := filepath.Join(cfg.BinDir, "kubectl") + kubeconfigPath := filepath.Join(cfg.ConfigDir, "kubeconfig.yaml") + + _ = exec.Command(kubectlPath, + "--kubeconfig", kubeconfigPath, + "create", "namespace", "obol-frontend", + "--dry-run=client", "-o", "yaml", + ).Run() + + patch := fmt.Sprintf(`{"data":{"obolVersion":%q}}`, version.Version) + cmd := exec.Command(kubectlPath, + "--kubeconfig", kubeconfigPath, + "patch", "configmap", "obol-stack-config", + "-n", "obol-frontend", + "--type", "merge", + "-p", patch, + ) + if out, err := cmd.CombinedOutput(); err == nil { + return nil + } else if !strings.Contains(string(out), "NotFound") && !strings.Contains(string(out), "not found") { + return fmt.Errorf("kubectl patch obol-stack-config failed: %w: %s", err, strings.TrimSpace(string(out))) + } + + // ConfigMap does not exist yet (no tunnel ever synced) — create with version only. + manifest := fmt.Sprintf(`apiVersion: v1 +kind: ConfigMap +metadata: + name: obol-stack-config + namespace: obol-frontend +data: + obolVersion: %s +`, version.Version) + create := exec.Command(kubectlPath, + "--kubeconfig", kubeconfigPath, + "apply", "--server-side", "--force-conflicts", "-f", "-", + ) + create.Stdin = strings.NewReader(manifest) + if out, err := create.CombinedOutput(); err != nil { + return fmt.Errorf("kubectl apply obol-stack-config failed: %w: %s", err, strings.TrimSpace(string(out))) + } + return nil +} + // EnsureTunnelForSell ensures the tunnel is running and propagates the URL to // the public service discovery surfaces needed by seller flows. It updates the // frontend ConfigMap and storefront, but deliberately avoids syncing the From cb6f2f24e17e93fab13ea5f09e58df5f0ea0a297 Mon Sep 17 00:00:00 2001 From: HananINouman Date: Sat, 25 Jul 2026 00:14:53 +0300 Subject: [PATCH 2/2] fix(tunnel): SSA-merge obolVersion without fake ns ensure Collapse SyncStackConfigVersion to a single server-side apply of obolVersion, quote ConfigMap data values, and drop the dry-run-only namespace create that never applied. Co-authored-by: Cursor --- internal/tunnel/tunnel.go | 49 ++++++++++----------------------------- 1 file changed, 12 insertions(+), 37 deletions(-) diff --git a/internal/tunnel/tunnel.go b/internal/tunnel/tunnel.go index f26a7ed7..aa21eddc 100644 --- a/internal/tunnel/tunnel.go +++ b/internal/tunnel/tunnel.go @@ -990,26 +990,20 @@ func freeLocalPort() (int, error) { // SyncTunnelConfigMap creates or patches the obol-stack-config ConfigMap in the // obol-frontend namespace with the current tunnel URL and the running Obol CLI // version. The frontend reads this ConfigMap for the public tunnel URL and the -// footer version display. +// footer version display. Server-side apply merges fields; call after +// obol-frontend exists (helmfile). func SyncTunnelConfigMap(cfg *config.Config, tunnelURL string) error { kubectlPath := filepath.Join(cfg.BinDir, "kubectl") kubeconfigPath := filepath.Join(cfg.ConfigDir, "kubeconfig.yaml") - // Ensure the namespace exists (non-fatal if it doesn't). - _ = exec.Command(kubectlPath, - "--kubeconfig", kubeconfigPath, - "create", "namespace", "obol-frontend", - "--dry-run=client", "-o", "yaml", - ).Run() - manifest := fmt.Sprintf(`apiVersion: v1 kind: ConfigMap metadata: name: obol-stack-config namespace: obol-frontend data: - tunnelURL: %s - obolVersion: %s + tunnelURL: %q + obolVersion: %q `, strings.TrimRight(tunnelURL, "/"), version.Version) // Server-side apply avoids the flaky client-side /openapi/v2 download on k3d. @@ -1026,47 +1020,28 @@ data: return nil } -// SyncStackConfigVersion patches obolVersion into obol-frontend/obol-stack-config -// without touching tunnelURL. Used on stack up when no tunnel sync runs yet. +// SyncStackConfigVersion SSA-merges only obolVersion into +// obol-frontend/obol-stack-config, leaving tunnelURL untouched. Used on stack +// up when no tunnel sync runs yet (after infra deploy creates the namespace). func SyncStackConfigVersion(cfg *config.Config) error { kubectlPath := filepath.Join(cfg.BinDir, "kubectl") kubeconfigPath := filepath.Join(cfg.ConfigDir, "kubeconfig.yaml") - _ = exec.Command(kubectlPath, - "--kubeconfig", kubeconfigPath, - "create", "namespace", "obol-frontend", - "--dry-run=client", "-o", "yaml", - ).Run() - - patch := fmt.Sprintf(`{"data":{"obolVersion":%q}}`, version.Version) - cmd := exec.Command(kubectlPath, - "--kubeconfig", kubeconfigPath, - "patch", "configmap", "obol-stack-config", - "-n", "obol-frontend", - "--type", "merge", - "-p", patch, - ) - if out, err := cmd.CombinedOutput(); err == nil { - return nil - } else if !strings.Contains(string(out), "NotFound") && !strings.Contains(string(out), "not found") { - return fmt.Errorf("kubectl patch obol-stack-config failed: %w: %s", err, strings.TrimSpace(string(out))) - } - - // ConfigMap does not exist yet (no tunnel ever synced) — create with version only. manifest := fmt.Sprintf(`apiVersion: v1 kind: ConfigMap metadata: name: obol-stack-config namespace: obol-frontend data: - obolVersion: %s + obolVersion: %q `, version.Version) - create := exec.Command(kubectlPath, + + cmd := exec.Command(kubectlPath, "--kubeconfig", kubeconfigPath, "apply", "--server-side", "--force-conflicts", "-f", "-", ) - create.Stdin = strings.NewReader(manifest) - if out, err := create.CombinedOutput(); err != nil { + cmd.Stdin = strings.NewReader(manifest) + if out, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("kubectl apply obol-stack-config failed: %w: %s", err, strings.TrimSpace(string(out))) } return nil