diff --git a/cmd/obol/main.go b/cmd/obol/main.go index 0b8e2652d..86a392a2b 100644 --- a/cmd/obol/main.go +++ b/cmd/obol/main.go @@ -325,178 +325,12 @@ GLOBAL OPTIONS:{{template "visibleFlagTemplate" .}}{{end}} // ============================================================ // Kubernetes Tool Passthroughs (with auto-configured KUBECONFIG) // ============================================================ - { - Name: "kubectl", - Usage: "Run kubectl with stack kubeconfig (passthrough)", - SkipFlagParsing: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - kubeconfigPath := filepath.Join(cfg.ConfigDir, "kubeconfig.yaml") - - // Check if kubeconfig exists - if _, err := os.Stat(kubeconfigPath); os.IsNotExist(err) { - return errors.New("stack not running, use 'obol stack up' first") - } - - kubectlPath := filepath.Join(cfg.BinDir, "kubectl") - - // Check if kubectl exists - if _, err := os.Stat(kubectlPath); os.IsNotExist(err) { - return fmt.Errorf("kubectl not found at %s", cfg.BinDir) - } - - // Execute kubectl directly with KUBECONFIG set - proc := exec.Command(kubectlPath, cmd.Args().Slice()...) - - proc.Env = append(os.Environ(), "KUBECONFIG="+kubeconfigPath) - proc.Stdin = os.Stdin - proc.Stdout = os.Stdout - proc.Stderr = os.Stderr - - if err := proc.Run(); err != nil { - // Preserve the exit code from kubectl - exitErr := &exec.ExitError{} - if errors.As(err, &exitErr) { - if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { - os.Exit(status.ExitStatus()) - } - } - - return err - } - - return nil - }, - }, - { - Name: "helm", - Usage: "Run helm with stack kubeconfig (passthrough)", - SkipFlagParsing: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - kubeconfigPath := filepath.Join(cfg.ConfigDir, "kubeconfig.yaml") - - // Check if kubeconfig exists - if _, err := os.Stat(kubeconfigPath); os.IsNotExist(err) { - return errors.New("stack not running, use 'obol stack up' first") - } - - helmPath := filepath.Join(cfg.BinDir, "helm") - - // Check if helm exists - if _, err := os.Stat(helmPath); os.IsNotExist(err) { - return fmt.Errorf("helm not found at %s", cfg.BinDir) - } - - // Execute helm directly with KUBECONFIG set - proc := exec.Command(helmPath, cmd.Args().Slice()...) - - proc.Env = append(os.Environ(), "KUBECONFIG="+kubeconfigPath) - proc.Stdin = os.Stdin - proc.Stdout = os.Stdout - proc.Stderr = os.Stderr - - if err := proc.Run(); err != nil { - // Preserve the exit code from helm - exitErr := &exec.ExitError{} - if errors.As(err, &exitErr) { - if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { - os.Exit(status.ExitStatus()) - } - } - - return err - } - - return nil - }, - }, - { - Name: "helmfile", - Usage: "Run helmfile with stack kubeconfig (passthrough)", - SkipFlagParsing: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - kubeconfigPath := filepath.Join(cfg.ConfigDir, "kubeconfig.yaml") - - // Check if kubeconfig exists - if _, err := os.Stat(kubeconfigPath); os.IsNotExist(err) { - return errors.New("stack not running, use 'obol stack up' first") - } - - helmfilePath := filepath.Join(cfg.BinDir, "helmfile") - - // Check if helmfile exists - if _, err := os.Stat(helmfilePath); os.IsNotExist(err) { - return fmt.Errorf("helmfile not found at %s", cfg.BinDir) - } - - // Execute helmfile directly with KUBECONFIG and HELMFILE_FILE_PATH set - helmfileConfigPath := filepath.Join(cfg.ConfigDir, "helmfile.yaml") - proc := exec.Command(helmfilePath, cmd.Args().Slice()...) - - proc.Env = append(os.Environ(), - "KUBECONFIG="+kubeconfigPath, - "HELMFILE_FILE_PATH="+helmfileConfigPath, - ) - proc.Stdin = os.Stdin - proc.Stdout = os.Stdout - proc.Stderr = os.Stderr - - if err := proc.Run(); err != nil { - // Preserve the exit code from helmfile - exitErr := &exec.ExitError{} - if errors.As(err, &exitErr) { - if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { - os.Exit(status.ExitStatus()) - } - } - - return err - } - - return nil - }, - }, - { - Name: "k9s", - Usage: "Run k9s with stack kubeconfig (passthrough)", - SkipFlagParsing: true, - Action: func(ctx context.Context, cmd *cli.Command) error { - kubeconfigPath := filepath.Join(cfg.ConfigDir, "kubeconfig.yaml") - - // Check if kubeconfig exists - if _, err := os.Stat(kubeconfigPath); os.IsNotExist(err) { - return errors.New("stack not running, use 'obol stack up' first") - } - - k9sPath := filepath.Join(cfg.BinDir, "k9s") - - // Check if k9s exists - if _, err := os.Stat(k9sPath); os.IsNotExist(err) { - return fmt.Errorf("k9s not found at %s", cfg.BinDir) - } - - // Execute k9s directly with KUBECONFIG set - proc := exec.Command(k9sPath, cmd.Args().Slice()...) - - proc.Env = append(os.Environ(), "KUBECONFIG="+kubeconfigPath) - proc.Stdin = os.Stdin - proc.Stdout = os.Stdout - proc.Stderr = os.Stderr - - if err := proc.Run(); err != nil { - // Preserve the exit code from k9s - exitErr := &exec.ExitError{} - if errors.As(err, &exitErr) { - if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { - os.Exit(status.ExitStatus()) - } - } - - return err - } - - return nil - }, - }, + passthroughCommand(cfg, "kubectl", nil), + passthroughCommand(cfg, "helm", nil), + passthroughCommand(cfg, "helmfile", func(cfg *config.Config) []string { + return []string{"HELMFILE_FILE_PATH=" + filepath.Join(cfg.ConfigDir, "helmfile.yaml")} + }), + passthroughCommand(cfg, "k9s", nil), // ============================================================ // Utility Commands // ============================================================ @@ -691,3 +525,42 @@ func debugReadBuildInfo() (string, bool) { } return bi.GoVersion, true } + +// passthroughCommand builds a CLI command that execs a bundled tool with +// KUBECONFIG pre-set. extraEnv, if non-nil, yields additional env vars at run time. +func passthroughCommand(cfg *config.Config, tool string, extraEnv func(*config.Config) []string) *cli.Command { + return &cli.Command{ + Name: tool, + Usage: "Run " + tool + " with stack kubeconfig (passthrough)", + SkipFlagParsing: true, + Action: func(ctx context.Context, cmd *cli.Command) error { + kubeconfigPath := filepath.Join(cfg.ConfigDir, "kubeconfig.yaml") + if _, err := os.Stat(kubeconfigPath); os.IsNotExist(err) { + return errors.New("stack not running, use 'obol stack up' first") + } + toolPath := filepath.Join(cfg.BinDir, tool) + if _, err := os.Stat(toolPath); os.IsNotExist(err) { + return fmt.Errorf("%s not found at %s", tool, cfg.BinDir) + } + + proc := exec.Command(toolPath, cmd.Args().Slice()...) + env := append(os.Environ(), "KUBECONFIG="+kubeconfigPath) + if extraEnv != nil { + env = append(env, extraEnv(cfg)...) + } + proc.Env = env + proc.Stdin, proc.Stdout, proc.Stderr = os.Stdin, os.Stdout, os.Stderr + + if err := proc.Run(); err != nil { + exitErr := &exec.ExitError{} + if errors.As(err, &exitErr) { + if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { + os.Exit(status.ExitStatus()) + } + } + return err + } + return nil + }, + } +} diff --git a/internal/serviceoffercontroller/controller.go b/internal/serviceoffercontroller/controller.go index 401fa6f0b..bf2ac2228 100644 --- a/internal/serviceoffercontroller/controller.go +++ b/internal/serviceoffercontroller/controller.go @@ -10,6 +10,7 @@ import ( "math/big" "net/http" "os" + "slices" "sort" "strings" "sync" @@ -349,7 +350,7 @@ func (c *Controller) reconcileOffer(ctx context.Context, key string) error { } if offer.DeletionTimestamp != nil { - if !containsFinalizer(raw, serviceOfferFinalizer) { + if !slices.Contains(raw.GetFinalizers(), serviceOfferFinalizer) { return nil } if err := c.reconcileDeletingOffer(ctx, offer); err != nil { @@ -369,7 +370,7 @@ func (c *Controller) reconcileOffer(ctx context.Context, key string) error { return c.removeFinalizer(ctx, raw, serviceOfferFinalizer) } - if !containsFinalizer(raw, serviceOfferFinalizer) { + if !slices.Contains(raw.GetFinalizers(), serviceOfferFinalizer) { return c.addFinalizer(ctx, raw, serviceOfferFinalizer) } @@ -377,11 +378,11 @@ func (c *Controller) reconcileOffer(ctx context.Context, key string) error { status.ObservedGeneration = offer.Generation status.Endpoint = offer.EffectivePath() - if err := c.reconcileModel(statusFor(&status), offer); err != nil { + if err := c.reconcileModel(&status, offer); err != nil { return err } - upstreamHealthy, err := c.reconcileUpstream(ctx, statusFor(&status), offer) + upstreamHealthy, err := c.reconcileUpstream(ctx, &status, offer) if err != nil { return err } @@ -393,11 +394,11 @@ func (c *Controller) reconcileOffer(ctx context.Context, key string) error { setCondition(&status, "PaymentGateReady", "False", "Paused", "Offer is paused") setCondition(&status, "RoutePublished", "False", "Paused", "Offer is paused") } else if upstreamHealthy && isConditionTrue(status, "ModelReady") { - if err := c.reconcilePaymentGate(ctx, statusFor(&status), offer); err != nil { + if err := c.reconcilePaymentGate(ctx, &status, offer); err != nil { return err } if isConditionTrue(status, "PaymentGateReady") { - if err := c.reconcileRoute(ctx, statusFor(&status), offer); err != nil { + if err := c.reconcileRoute(ctx, &status, offer); err != nil { return err } } @@ -406,7 +407,7 @@ func (c *Controller) reconcileOffer(ctx context.Context, key string) error { setCondition(&status, "RoutePublished", "False", "WaitingForPaymentGate", "Waiting for payment gate before publishing route") } - if err := c.reconcileRegistrationStatus(ctx, statusFor(&status), offer); err != nil { + if err := c.reconcileRegistrationStatus(ctx, &status, offer); err != nil { return err } @@ -880,7 +881,7 @@ func (c *Controller) reconcileRegistrationActive(ctx context.Context, raw *unstr status.RegistrationOwner = firstNonEmpty(status.RegistrationOwner, c.registrationOwnerAddress) status.RegistrationURI = firstNonEmpty(status.RegistrationURI, status.PublishedURL) if agentID != "" && c.registrationKey != nil && client != nil && !status.MetadataSynced { - agentIDBig, ok := newBigInt(agentID) + agentIDBig, ok := new(big.Int).SetString(strings.TrimSpace(agentID), 10) if !ok { return fmt.Errorf("invalid agent id %q", agentID) } @@ -964,7 +965,7 @@ func (c *Controller) reconcileRegistrationTombstone(ctx context.Context, raw *un } defer client.Close() - agentIDBig, ok := newBigInt(agentID) + agentIDBig, ok := new(big.Int).SetString(strings.TrimSpace(agentID), 10) if !ok { return fmt.Errorf("invalid agent id %q", agentID) } @@ -1302,14 +1303,7 @@ func (c *Controller) addFinalizer(ctx context.Context, raw *unstructured.Unstruc func (c *Controller) removeFinalizer(ctx context.Context, raw *unstructured.Unstructured, finalizer string) error { patched := raw.DeepCopy() - finalizers := patched.GetFinalizers() - filtered := finalizers[:0] - for _, item := range finalizers { - if item != finalizer { - filtered = append(filtered, item) - } - } - patched.SetFinalizers(filtered) + patched.SetFinalizers(slices.DeleteFunc(patched.GetFinalizers(), func(s string) bool { return s == finalizer })) _, err := c.offers.Namespace(patched.GetNamespace()).Update(ctx, patched, metav1.UpdateOptions{}) return err } @@ -1330,15 +1324,6 @@ func (c *Controller) registrationBaseURL(ctx context.Context) (string, error) { return c.defaultBaseURL, nil } -func containsFinalizer(raw *unstructured.Unstructured, finalizer string) bool { - for _, item := range raw.GetFinalizers() { - if item == finalizer { - return true - } - } - return false -} - func decodeServiceOffer(raw *unstructured.Unstructured) (*monetizeapi.ServiceOffer, error) { var offer monetizeapi.ServiceOffer if err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &offer); err != nil { @@ -1370,10 +1355,6 @@ func asUnstructured(obj any) *unstructured.Unstructured { return nil } -func statusFor(status *monetizeapi.ServiceOfferStatus) *monetizeapi.ServiceOfferStatus { - return status -} - func requestPhaseReady(phase string) bool { return phase == registrationPhaseRegistered } @@ -1399,11 +1380,6 @@ func truncateMessage(message string) string { return message[:200] } -func newBigInt(value string) (*big.Int, bool) { - parsed, ok := new(big.Int).SetString(strings.TrimSpace(value), 10) - return parsed, ok -} - func loadRegistrationSigningKey() (*ecdsa.PrivateKey, error) { keyHex := strings.TrimSpace(os.Getenv("ERC8004_PRIVATE_KEY")) if keyHex == "" { diff --git a/internal/serviceoffercontroller/purchase.go b/internal/serviceoffercontroller/purchase.go index 340ea810c..77b7b3381 100644 --- a/internal/serviceoffercontroller/purchase.go +++ b/internal/serviceoffercontroller/purchase.go @@ -7,6 +7,7 @@ import ( "io" "log" "net/http" + "slices" "strings" "time" @@ -35,7 +36,7 @@ func (c *Controller) reconcilePurchase(ctx context.Context, key string) error { } // Add finalizer if missing. - if !hasStringInSlice(raw.GetFinalizers(), purchaseRequestFinalizer) { + if !slices.Contains(raw.GetFinalizers(), purchaseRequestFinalizer) { patched := raw.DeepCopy() patched.SetFinalizers(append(patched.GetFinalizers(), purchaseRequestFinalizer)) if _, err := c.dynClient.Resource(monetizeapi.PurchaseRequestGVR).Namespace(ns).Update(ctx, patched, metav1.UpdateOptions{}); err != nil { @@ -364,15 +365,6 @@ func (c *Controller) updatePurchaseStatus(ctx context.Context, raw *unstructured return err } -func hasStringInSlice(slice []string, target string) bool { - for _, s := range slice { - if s == target { - return true - } - } - return false -} - func purchaseConditionIsTrue(conditions []monetizeapi.Condition, condType string) bool { for _, c := range conditions { if c.Type == condType { diff --git a/internal/serviceoffercontroller/purchase_helpers.go b/internal/serviceoffercontroller/purchase_helpers.go index d7762232b..33526acb1 100644 --- a/internal/serviceoffercontroller/purchase_helpers.go +++ b/internal/serviceoffercontroller/purchase_helpers.go @@ -194,22 +194,27 @@ func (c *Controller) litellmModelIDsByName(ctx context.Context, ns, masterKey, m // ── ConfigMap merge (optimistic concurrency) ──────────────────────────────── -func (c *Controller) mergeBuyerConfig(ctx context.Context, ns, name string, upstream map[string]any) error { - cm, err := c.kubeClient.CoreV1().ConfigMaps(ns).Get(ctx, buyerConfigCM, metav1.GetOptions{}) +// mergeBuyerCM writes payload as JSON under ".json" in the given ConfigMap, +// removing legacyKey on the way so callers migrate off the old shared-key shape. +func (c *Controller) mergeBuyerCM(ctx context.Context, ns, cmName, legacyKey, name string, payload any) error { + cm, err := c.kubeClient.CoreV1().ConfigMaps(ns).Get(ctx, cmName, metav1.GetOptions{}) if err != nil { - return fmt.Errorf("get %s/%s: %w", ns, buyerConfigCM, err) + return fmt.Errorf("get %s/%s: %w", ns, cmName, err) } if cm.Data == nil { cm.Data = make(map[string]string) } - delete(cm.Data, "config.json") - configJSON, _ := json.MarshalIndent(upstream, "", " ") - cm.Data[name+".json"] = string(configJSON) - + delete(cm.Data, legacyKey) + data, _ := json.Marshal(payload) + cm.Data[name+".json"] = string(data) _, err = c.kubeClient.CoreV1().ConfigMaps(ns).Update(ctx, cm, metav1.UpdateOptions{}) return err } +func (c *Controller) mergeBuyerConfig(ctx context.Context, ns, name string, upstream map[string]any) error { + return c.mergeBuyerCM(ctx, ns, buyerConfigCM, "config.json", name, upstream) +} + func otherActivePurchaseUsesModel(purchases []*monetizeapi.PurchaseRequest, namespace, name, modelName string) *monetizeapi.PurchaseRequest { for _, pr := range purchases { if pr == nil { @@ -244,43 +249,25 @@ func (c *Controller) findOtherActivePurchaseForModel(namespace, name, modelName } func (c *Controller) mergeBuyerAuths(ctx context.Context, ns, name string, auths []map[string]string) error { - cm, err := c.kubeClient.CoreV1().ConfigMaps(ns).Get(ctx, buyerAuthsCM, metav1.GetOptions{}) - if err != nil { - return fmt.Errorf("get %s/%s: %w", ns, buyerAuthsCM, err) - } - if cm.Data == nil { - cm.Data = make(map[string]string) - } - delete(cm.Data, "auths.json") - authsJSON, _ := json.MarshalIndent(auths, "", " ") - cm.Data[name+".json"] = string(authsJSON) - - _, err = c.kubeClient.CoreV1().ConfigMaps(ns).Update(ctx, cm, metav1.UpdateOptions{}) - return err + return c.mergeBuyerCM(ctx, ns, buyerAuthsCM, "auths.json", name, auths) } func (c *Controller) removeBuyerUpstream(ctx context.Context, ns, name string) { - // Remove from config. - cm, err := c.kubeClient.CoreV1().ConfigMaps(ns).Get(ctx, buyerConfigCM, metav1.GetOptions{}) - if err == nil { + for _, spec := range []struct{ cm, legacy string }{ + {buyerConfigCM, "config.json"}, + {buyerAuthsCM, "auths.json"}, + } { + cm, err := c.kubeClient.CoreV1().ConfigMaps(ns).Get(ctx, spec.cm, metav1.GetOptions{}) + if err != nil { + continue + } if cm.Data == nil { cm.Data = make(map[string]string) } - delete(cm.Data, "config.json") + delete(cm.Data, spec.legacy) delete(cm.Data, name+".json") c.kubeClient.CoreV1().ConfigMaps(ns).Update(ctx, cm, metav1.UpdateOptions{}) } - - // Remove from auths. - authsCM, err := c.kubeClient.CoreV1().ConfigMaps(ns).Get(ctx, buyerAuthsCM, metav1.GetOptions{}) - if err == nil { - if authsCM.Data == nil { - authsCM.Data = make(map[string]string) - } - delete(authsCM.Data, "auths.json") - delete(authsCM.Data, name+".json") - c.kubeClient.CoreV1().ConfigMaps(ns).Update(ctx, authsCM, metav1.UpdateOptions{}) - } } // addLiteLLMModelEntry adds a paid/ route to LiteLLM. Writes the diff --git a/internal/serviceoffercontroller/purchase_lifecycle_test.go b/internal/serviceoffercontroller/purchase_lifecycle_test.go index 6c8b43ac0..65f96bd26 100644 --- a/internal/serviceoffercontroller/purchase_lifecycle_test.go +++ b/internal/serviceoffercontroller/purchase_lifecycle_test.go @@ -7,6 +7,7 @@ import ( "io" "net/http" "net/http/httptest" + "slices" "strings" "sync" "testing" @@ -297,7 +298,7 @@ func TestReconcilePurchaseHappyPath(t *testing.T) { } got := getPurchaseRequest(t, c, "agent-ns", "solo") - if !containsFinalizer(mustPurchaseObject(t, *got), purchaseRequestFinalizer) { + if !slices.Contains(mustPurchaseObject(t, *got).GetFinalizers(), purchaseRequestFinalizer) { t.Fatal("purchase finalizer missing after reconcile") } if purchaseCondition(t, got, "Probed").Status != "True" { @@ -363,7 +364,7 @@ func TestReconcilePurchaseAddsFinalizerOnFirstPass(t *testing.T) { } got := getPurchaseRequest(t, c, "agent-ns", "solo") - if !containsFinalizer(mustPurchaseObject(t, *got), purchaseRequestFinalizer) { + if !slices.Contains(mustPurchaseObject(t, *got).GetFinalizers(), purchaseRequestFinalizer) { t.Fatal("purchase finalizer missing after first reconcile") } if len(got.Status.Conditions) != 0 { @@ -880,7 +881,7 @@ func TestReconcileDeletingPurchaseDrainsUntilRemainingZero(t *testing.T) { } got := getPurchaseRequest(t, c, "agent-ns", "alpha") - if !containsFinalizer(mustPurchaseObject(t, *got), purchaseRequestFinalizer) { + if !slices.Contains(mustPurchaseObject(t, *got).GetFinalizers(), purchaseRequestFinalizer) { t.Fatal("finalizer removed before auth pool drained") } deleting := purchaseCondition(t, got, "Deleting") @@ -1006,7 +1007,7 @@ func TestReconcileDeletingPurchaseWaitsForRuntimeStatusToDisappear(t *testing.T) } got := getPurchaseRequest(t, c, "agent-ns", "alpha") - if !containsFinalizer(mustPurchaseObject(t, *got), purchaseRequestFinalizer) { + if !slices.Contains(mustPurchaseObject(t, *got).GetFinalizers(), purchaseRequestFinalizer) { t.Fatal("finalizer removed before runtime status disappeared") } deleting := purchaseCondition(t, got, "Deleting") diff --git a/internal/serviceoffercontroller/render.go b/internal/serviceoffercontroller/render.go index 323e609f8..b2e4b033a 100644 --- a/internal/serviceoffercontroller/render.go +++ b/internal/serviceoffercontroller/render.go @@ -23,27 +23,6 @@ const ( skillCatalogRouteName = "obol-skill-md-route" ) -func buildMiddleware(offer *monetizeapi.ServiceOffer) *unstructured.Unstructured { - obj := &unstructured.Unstructured{ - Object: map[string]any{ - "apiVersion": "traefik.io/v1alpha1", - "kind": "Middleware", - "metadata": map[string]any{ - "name": "x402-" + offer.Name, - "namespace": offer.Namespace, - "ownerReferences": []any{ownerRefMap(offer)}, - }, - "spec": map[string]any{ - "forwardAuth": map[string]any{ - "address": "http://x402-verifier.x402.svc.cluster.local:8080/verify", - "authResponseHeaders": []any{"X-Payment-Status", "X-Payment-Tx", "Authorization"}, - }, - }, - }, - } - return obj -} - func buildRegistrationRequest(offer *monetizeapi.ServiceOffer, desiredState string) *unstructured.Unstructured { return &unstructured.Unstructured{ Object: map[string]any{ @@ -496,10 +475,6 @@ func registrationRouteName(name string) string { return safeName("so-", name, "-wellknown") } -func ownerRef(offer *monetizeapi.ServiceOffer) metav1.OwnerReference { - return ownerRefFor(monetizeapi.Group+"/"+monetizeapi.Version, monetizeapi.ServiceOfferKind, offer.Name, offer.UID) -} - func ownerRefMap(offer *monetizeapi.ServiceOffer) map[string]any { return ownerRefMapFor(monetizeapi.Group+"/"+monetizeapi.Version, monetizeapi.ServiceOfferKind, offer.Name, offer.UID) } @@ -508,18 +483,6 @@ func registrationRequestOwnerRefMap(request *monetizeapi.RegistrationRequest) ma return ownerRefMapFor(monetizeapi.Group+"/"+monetizeapi.Version, monetizeapi.RegistrationRequestKind, request.Name, request.UID) } -func ownerRefFor(apiVersion, kind, name string, uid types.UID) metav1.OwnerReference { - trueValue := true - return metav1.OwnerReference{ - APIVersion: apiVersion, - Kind: kind, - Name: name, - UID: uid, - Controller: &trueValue, - BlockOwnerDeletion: &trueValue, - } -} - func ownerRefMapFor(apiVersion, kind, name string, uid types.UID) map[string]any { return map[string]any{ "apiVersion": apiVersion, diff --git a/internal/stack/stack.go b/internal/stack/stack.go index 29e87a349..7dc0063ae 100644 --- a/internal/stack/stack.go +++ b/internal/stack/stack.go @@ -32,21 +32,14 @@ const ( // Init initializes the stack configuration func Init(cfg *config.Config, u *ui.UI, force bool, backendName string) error { - // Check if any stack config already exists + // Check if any stack config already exists (legacy k3d.yaml included). stackIDPath := filepath.Join(cfg.ConfigDir, stackIDFile) - backendFilePath := filepath.Join(cfg.ConfigDir, stackBackendFile) - hasExistingConfig := false - if _, err := os.Stat(stackIDPath); err == nil { - hasExistingConfig = true - } - - if _, err := os.Stat(backendFilePath); err == nil { - hasExistingConfig = true - } - // Also check legacy k3d.yaml for backward compatibility - if _, err := os.Stat(filepath.Join(cfg.ConfigDir, k3dConfigFile)); err == nil { - hasExistingConfig = true + for _, f := range []string{stackIDFile, stackBackendFile, k3dConfigFile} { + if _, err := os.Stat(filepath.Join(cfg.ConfigDir, f)); err == nil { + hasExistingConfig = true + break + } } if hasExistingConfig && !force { diff --git a/internal/testutil/eip712_signer.go b/internal/testutil/eip712_signer.go index 15ccd8ef2..9950a5c2e 100644 --- a/internal/testutil/eip712_signer.go +++ b/internal/testutil/eip712_signer.go @@ -285,19 +285,6 @@ func SignPaymentHeaderDirect(signerKeyHex, payTo, amount string, chainID int64) return base64.StdEncoding.EncodeToString(data) } -func chainName(chainID int64) string { - switch chainID { - case 84532: - return "base-sepolia" - case 8453: - return "base" - case 1: - return "ethereum" - default: - return fmt.Sprintf("eip155:%d", chainID) - } -} - func chainCAIP2(chainID int64) string { switch chainID { case 84532: diff --git a/internal/x402/buyer/proxy.go b/internal/x402/buyer/proxy.go index 5b2e7755f..97f6b44bf 100644 --- a/internal/x402/buyer/proxy.go +++ b/internal/x402/buyer/proxy.go @@ -70,15 +70,7 @@ func NewProxy(cfg *Config, auths AuthsFile, state *StateStore) (*Proxy, error) { reloadCh: make(chan struct{}, 1), } - p.mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - fmt.Fprint(w, "ok") - }) - p.mux.HandleFunc("GET /status", p.handleStatus) - p.mux.HandleFunc("POST /admin/reload", p.handleAdminReload) - p.mux.HandleFunc("POST /admin/remove", p.handleAdminRemove) - p.mux.Handle("GET /metrics", p.metrics.handler()) - registerOpenAIRoutes(p.mux, p.handleModelRequest) + p.registerCoreRoutes() if err := p.Reload(cfg, auths); err != nil { return nil, err @@ -159,6 +151,17 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (p *Proxy) syncCompatibilityRoutesLocked() { p.mux = http.NewServeMux() + p.registerCoreRoutes() + for name, upstream := range p.upstreams { + prefix := fmt.Sprintf("/upstream/%s/", name) + p.mux.Handle(prefix, http.StripPrefix(strings.TrimSuffix(prefix, "/"), upstream.handler)) + } +} + +// registerCoreRoutes wires the built-in /healthz, /status, /admin/*, /metrics, +// and OpenAI-compatible routes onto p.mux. Called both at construction and on +// every reload (since reload rebuilds the mux to drop stale upstream routes). +func (p *Proxy) registerCoreRoutes() { p.mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) fmt.Fprint(w, "ok") @@ -168,11 +171,6 @@ func (p *Proxy) syncCompatibilityRoutesLocked() { p.mux.HandleFunc("POST /admin/remove", p.handleAdminRemove) p.mux.Handle("GET /metrics", p.metrics.handler()) registerOpenAIRoutes(p.mux, p.handleModelRequest) - - for name, upstream := range p.upstreams { - prefix := fmt.Sprintf("/upstream/%s/", name) - p.mux.Handle(prefix, http.StripPrefix(strings.TrimSuffix(prefix, "/"), upstream.handler)) - } } func (p *Proxy) syncMetricsLocked() { @@ -327,14 +325,11 @@ func (p *Proxy) resolveModelRequest(body []byte) (string, []byte, *upstreamEntry func normalizeRemoteModel(model string) string { normalized := strings.TrimSpace(model) - for { - switch { - case strings.HasPrefix(normalized, "paid/"): - normalized = strings.TrimPrefix(normalized, "paid/") - case strings.HasPrefix(normalized, "openai/"): - normalized = strings.TrimPrefix(normalized, "openai/") - default: + before := normalized + normalized = strings.TrimPrefix(normalized, "paid/") + normalized = strings.TrimPrefix(normalized, "openai/") + if normalized == before { return normalized } }