Skip to content
Open
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
12 changes: 6 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,15 +151,15 @@ obol
2. Parses each network's `helmfile.yaml.gotmpl` for environment variable annotations
3. Generates CLI flags automatically from annotations:
```yaml
# @enum mainnet,sepolia,holesky,hoodi
# @enum mainnet,sepolia,hoodi
# @default mainnet
# @description Blockchain network to deploy
- network: {{.Network}}
```
Becomes: `--network` flag with enum validation and default value

**Network install flow**:
1. User runs: `obol network install ethereum --network=holesky --execution-client=geth`
1. User runs: `obol network install ethereum --network=hoodi --execution-client=geth`
2. CLI collects flag values into `overrides` map
3. Validates enum constraints
4. Calls `network.Install(cfg, "ethereum", overrides)`
Expand Down Expand Up @@ -245,7 +245,7 @@ networks/

`values.yaml.gotmpl` contains configuration fields with annotations:
```yaml
# @enum mainnet,sepolia,holesky,hoodi
# @enum mainnet,sepolia,hoodi
# @default mainnet
# @description Blockchain network to deploy
network: {{.Network}}
Expand Down Expand Up @@ -322,15 +322,15 @@ obol network install ethereum --id prod --network=mainnet

# Multiple deployments with different configs
obol network install ethereum --id mainnet-01
obol network install ethereum --id holesky-test --network=holesky
obol network install ethereum --id hoodi-test --network=hoodi
# Both run simultaneously, isolated in separate namespaces
```

### Network Configuration Flow

1. **Install** (config generation only):
```
obol network install ethereum --network=holesky --execution-client=geth --id my-node
obol network install ethereum --network=hoodi --execution-client=geth --id my-node
Check if directory exists: ~/.config/obol/networks/ethereum/my-node/ (fail unless --force)
Expand Down Expand Up @@ -727,7 +727,7 @@ obol network list
obol network install ethereum --help

# Install with specific config
obol network install ethereum --network=holesky --execution-client=geth
obol network install ethereum --network=hoodi --execution-client=geth

# Verify deployment
obol kubectl get namespaces | grep ethereum
Expand Down
24 changes: 9 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ obol network install ethereum
obol network sync ethereum/nervous-otter

# Install another network configuration
obol network install ethereum --network=holesky
obol network install ethereum --network=hoodi
# This creates a separate deployment like: ethereum-happy-panda

# Deploy the second network
Expand Down Expand Up @@ -200,23 +200,17 @@ obol network install ethereum --network=mainnet --execution-client=geth --consen
# Deploy to cluster
obol network sync ethereum/nervous-otter

# Install Holesky testnet with Reth + Lighthouse
obol network install ethereum --network=holesky --execution-client=reth --consensus-client=lighthouse
# Creates configuration: ethereum-laughing-elephant
# Install Hoodi testnet with Reth + Lighthouse
obol network install ethereum --network=hoodi --execution-client=reth --consensus-client=lighthouse
# Creates: ethereum-laughing-elephant namespace

# Deploy Holesky to cluster
obol network sync ethereum/laughing-elephant

# Install another Holesky instance for testing
obol network install ethereum --network=holesky
# Creates configuration: ethereum-happy-panda

# Deploy second Holesky instance
obol network sync ethereum/happy-panda
# Install another Hoodi instance for testing
obol network install ethereum --network=hoodi
# Creates: ethereum-happy-panda namespace
```

**Ethereum configuration options:**
- `--network`: Choose network (mainnet, sepolia, holesky, hoodi)
- `--network`: Choose network (mainnet, sepolia, hoodi)
- `--execution-client`: Choose execution client (reth, geth, nethermind, besu, erigon, ethereumjs)
- `--consensus-client`: Choose consensus client (lighthouse, prysm, teku, nimbus, lodestar, grandine)

Expand Down Expand Up @@ -708,7 +702,7 @@ The stack will include [eRPC](https://erpc.cloud/), a specialized Ethereum load

Network deployments will register their endpoints with ERPC, enabling seamless access to blockchain data across all deployed instances. For example:
- `http://erpc.defaults.svc.cluster.local/ethereum/mainnet` → routes to mainnet deployment
- `http://erpc.defaults.svc.cluster.local/ethereum/holesky` → routes to holesky deployment
- `http://erpc.defaults.svc.cluster.local/ethereum/hoodi` → routes to hoodi deployment

### Advanced Tooling

Expand Down
29 changes: 28 additions & 1 deletion cmd/obol/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"path/filepath"
"syscall"

"github.com/ObolNetwork/obol-stack/internal/agent"
"github.com/ObolNetwork/obol-stack/internal/app"
"github.com/ObolNetwork/obol-stack/internal/config"
"github.com/ObolNetwork/obol-stack/internal/stack"
Expand Down Expand Up @@ -43,7 +44,8 @@ COMMANDS:
stack up Start the Obol Stack
stack down Stop the Obol Stack
stack purge Delete stack config (use --force to also delete data)

Obol Agent:
agent init Initialize the Obol Agent with an API key
Network Management:
network list List available networks
network install Install and deploy network to cluster
Expand Down Expand Up @@ -130,6 +132,31 @@ GLOBAL OPTIONS:
},
},
// ============================================================
// Obol Agent Commands
// ============================================================
{
Name: "agent",
Usage: "Manage Obol Agent",
Subcommands: []*cli.Command{
{
Name: "init",
Usage: "Initialize the Obol Agent with an API key",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "agent-api-key",
Aliases: []string{"a"},
Usage: "API key for the Obol Agent",
EnvVars: []string{"AGENT_API_KEY"},
},
},
Action: func(c *cli.Context) error {
agentAPIKey := c.String("agent-api-key")
return agent.Init(cfg, agentAPIKey)
},
},
},
},
// ============================================================
// Kubernetes Tool Passthroughs (with auto-configured KUBECONFIG)
// ============================================================
{
Expand Down
91 changes: 91 additions & 0 deletions internal/agent/agent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package agent

import (
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/ObolNetwork/obol-stack/internal/config"
"github.com/ObolNetwork/obol-stack/internal/stack"
)

const (
kubeconfigFile = "kubeconfig.yaml"
)

// Init initializes the Obol Agent with required secrets
func Init(cfg *config.Config, agentAPIKey string) error {
kubeconfigPath := filepath.Join(cfg.ConfigDir, kubeconfigFile)

// Check if kubeconfig exists (stack must be running)
if _, err := os.Stat(kubeconfigPath); os.IsNotExist(err) {
return fmt.Errorf("stack not running, use 'obol stack up' first")
}

// Get stack ID for logging
stackID := stack.GetStackID(cfg)
if stackID == "" {
return fmt.Errorf("stack ID not found, run 'obol stack init' first")
}

// If no API key provided via flag, try to read from stdin
if agentAPIKey == "" {
stat, _ := os.Stdin.Stat()
if (stat.Mode() & os.ModeCharDevice) == 0 {
// Data is being piped to stdin
data, err := io.ReadAll(os.Stdin)
if err == nil {
agentAPIKey = strings.TrimSpace(string(data))
}
}
}

// Validate Agent API key was provided
if agentAPIKey == "" {
return fmt.Errorf("agent API key required via --agent-api-key flag or AGENT_API_KEY environment variable. Navigate to https://aistudio.google.com/api-keys to create an API key for your Obol Agent")
}

fmt.Println("Initializing Obol Agent")
fmt.Printf("Stack ID: %s\n", stackID)
fmt.Println("Creating API key secret for Obol Agent")

kubectlPath := filepath.Join(cfg.BinDir, "kubectl")

// Create namespace (idempotent)
nsCmd := exec.Command(kubectlPath, "--kubeconfig", kubeconfigPath, "create", "namespace", "agent", "--dry-run=client", "-o", "yaml")
nsYAML, err := nsCmd.Output()
if err != nil {
return fmt.Errorf("failed to generate namespace manifest: %w", err)
}

applyNs := exec.Command(kubectlPath, "--kubeconfig", kubeconfigPath, "apply", "-f", "-")
applyNs.Stdin = strings.NewReader(string(nsYAML))
applyNs.Stdout = os.Stdout
applyNs.Stderr = os.Stderr
if err := applyNs.Run(); err != nil {
return fmt.Errorf("failed to create agent namespace: %w", err)
}

// Create secret (idempotent)
secretCmd := exec.Command(kubectlPath, "--kubeconfig", kubeconfigPath, "create", "secret", "generic", "obol-agent-api-key", "--from-literal=AGENT_API_KEY="+agentAPIKey, "--namespace=agent", "--dry-run=client", "-o", "yaml")
secretYAML, err := secretCmd.Output()
if err != nil {
return fmt.Errorf("failed to generate secret manifest: %w", err)
}

applySecret := exec.Command(kubectlPath, "--kubeconfig", kubeconfigPath, "apply", "-f", "-")
applySecret.Stdin = strings.NewReader(string(secretYAML))
applySecret.Stdout = os.Stdout
applySecret.Stderr = os.Stderr
if err := applySecret.Run(); err != nil {
return fmt.Errorf("failed to create Agent API key secret: %w", err)
}

fmt.Println("Agent API key secret created")
fmt.Println("Obol Agent initialized successfully")

return nil
}
Loading