Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 45 additions & 172 deletions cmd/obol/main.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
// ============================================================
Expand DownExpand Up@@ -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
},
}
}
46 changes: 11 additions & 35 deletions internal/serviceoffercontroller/controller.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,7 @@ import (
"math/big"
"net/http"
"os"
"slices"
"sort"
"strings"
"sync"
Expand DownExpand Up@@ -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 {
Expand All@@ -369,19 +370,19 @@ 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)
}

status := offer.Status
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
}
Expand All@@ -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
}
}
Expand All@@ -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
}

Expand DownExpand Up@@ -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)
}
Expand DownExpand Up@@ -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)
}
Expand DownExpand Up@@ -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
}
Expand All@@ -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 {
Expand DownExpand Up@@ -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
}
Expand All@@ -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 == "" {
Expand Down
12 changes: 2 additions & 10 deletions internal/serviceoffercontroller/purchase.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import (
"io"
"log"
"net/http"
"slices"
"strings"
"time"

Expand DownExpand Up@@ -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 {
Expand DownExpand Up@@ -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 {
Expand Down
Loading
Loading