diff --git a/CLAUDE.md b/CLAUDE.md index d19991d..bc40752 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -151,7 +151,7 @@ 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}} @@ -159,7 +159,7 @@ obol 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)` @@ -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}} @@ -322,7 +322,7 @@ 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 ``` @@ -330,7 +330,7 @@ obol network install ethereum --id holesky-test --network=holesky 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) ↓ @@ -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 diff --git a/README.md b/README.md index 201b3ef..0f24b0d 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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) @@ -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 diff --git a/cmd/obol/main.go b/cmd/obol/main.go index 3bd70f2..cde6626 100644 --- a/cmd/obol/main.go +++ b/cmd/obol/main.go @@ -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" @@ -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 @@ -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) // ============================================================ { diff --git a/internal/agent/agent.go b/internal/agent/agent.go new file mode 100644 index 0000000..ada5b2e --- /dev/null +++ b/internal/agent/agent.go @@ -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 +} diff --git a/internal/embed/infrastructure/base/templates/obol-agent.yaml b/internal/embed/infrastructure/base/templates/obol-agent.yaml new file mode 100644 index 0000000..f73dda7 --- /dev/null +++ b/internal/embed/infrastructure/base/templates/obol-agent.yaml @@ -0,0 +1,182 @@ +--- +# Obol Agent Kubernetes Manifest +# This manifest deploys the Obol AI Agent with namespace-scoped RBAC permissions +# The agent can read cluster-wide resources (nodes, namespaces) but can only modify +# resources in specific namespaces: agent (and others via dynamic bindings) + +#------------------------------------------------------------------------------ +# Namespace - Ensure the agent namespace exists +#------------------------------------------------------------------------------ +apiVersion: v1 +kind: Namespace +metadata: + name: agent + +--- +#------------------------------------------------------------------------------ +# ServiceAccount - Identity for the Obol Agent pod +#------------------------------------------------------------------------------ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: obol-agent + namespace: agent + +--- +#------------------------------------------------------------------------------ +# ClusterRole - Read-only access to cluster-wide resources +# Allows the agent to list namespaces and nodes across the entire cluster +#------------------------------------------------------------------------------ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: obol-agent-cluster-reader +rules: + - apiGroups: [""] + resources: ["namespaces", "nodes"] + verbs: ["get", "list", "watch"] # Read-only access + +--- +#------------------------------------------------------------------------------ +# ClusterRoleBinding - Grants cluster-wide read access to the agent +#------------------------------------------------------------------------------ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: obol-agent-cluster-reader-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: obol-agent-cluster-reader +subjects: + - kind: ServiceAccount + name: obol-agent + namespace: agent + +--- +#------------------------------------------------------------------------------ +# Role for 'agent' namespace +# Grants create/update/patch permissions within the agent's own namespace +#------------------------------------------------------------------------------ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: obol-agent-role + namespace: agent +rules: + - apiGroups: [""] # Core API group + resources: ["pods", "services", "endpoints", "persistentvolumeclaims", "configmaps", "secrets"] + verbs: ["get", "list", "watch", "create", "update", "patch"] + - apiGroups: ["apps"] # Apps API group + resources: ["deployments", "statefulsets", "daemonsets", "replicasets"] + verbs: ["get", "list", "watch", "create", "update", "patch"] + - apiGroups: ["batch"] # Batch API group + resources: ["jobs", "cronjobs"] + verbs: ["get", "list", "watch", "create", "update", "patch"] + - apiGroups: [""] + resources: ["pods/log"] # Access to pod logs + verbs: ["get"] + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: obol-agent-binding + namespace: agent +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: obol-agent-role +subjects: + - kind: ServiceAccount + name: obol-agent + namespace: agent + +--- +#------------------------------------------------------------------------------ +# Deployment - Obol Agent Application +# The agent provides AI-powered Kubernetes and Obol cluster management via MCP +#------------------------------------------------------------------------------ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: obol-agent + namespace: agent + labels: + app: obol-agent +spec: + replicas: 1 # Single instance deployment + selector: + matchLabels: + app: obol-agent + template: + metadata: + labels: + app: obol-agent + spec: + serviceAccountName: obol-agent # Uses the ServiceAccount created above for RBAC + containers: + - name: obol-agent + image: us-east4-docker.pkg.dev/prj-d-playgrounds-f0cb/obol-agent/obol-agent-ag-ui:latest + imagePullPolicy: Always # Always pull latest image + ports: + - name: http + containerPort: 8000 + protocol: TCP + env: + # REQUIRED: Agent API key from Kubernetes secret + # Secret created via: obol agent init --agent-api-key= + - name: AGENT_API_KEY + valueFrom: + secretKeyRef: + name: obol-agent-api-key + key: AGENT_API_KEY + optional: true # Allow deployment even if secret doesn't exist + + # PUBLIC_MODE controls Kubernetes MCP access + # false = Enable Kubernetes API access (uses RBAC permissions above) + # true = Disable Kubernetes API access (for public deployments) + - name: PUBLIC_MODE + value: "false" + + # Health checks ensure the pod is ready to receive traffic + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 10 + periodSeconds: 5 + + # Resource limits prevent the agent from consuming too many cluster resources + resources: + limits: # Maximum allowed resources + cpu: 2000m # 2 CPU cores + memory: 4Gi # 4 GiB RAM + +--- +#------------------------------------------------------------------------------ +# Service - Exposes the Obol Agent within the cluster +# Access the agent at: http://obol-agent.agent.svc.cluster.local:8000 +#------------------------------------------------------------------------------ +apiVersion: v1 +kind: Service +metadata: + name: obol-agent + namespace: agent + labels: + app: obol-agent +spec: + type: ClusterIP # Internal cluster access only (use Ingress for external access) + ports: + - port: 8000 # Service port + targetPort: http # Container port name + protocol: TCP + name: http + selector: + app: obol-agent # Routes traffic to pods with this label \ No newline at end of file diff --git a/internal/embed/infrastructure/values/obol-frontend.yaml.gotmpl b/internal/embed/infrastructure/values/obol-frontend.yaml.gotmpl index f7a69f1..3301156 100644 --- a/internal/embed/infrastructure/values/obol-frontend.yaml.gotmpl +++ b/internal/embed/infrastructure/values/obol-frontend.yaml.gotmpl @@ -13,7 +13,7 @@ image: repository: obolnetwork/obol-stack-front-end pullPolicy: Always - tag: "latest" + tag: "v0.1.1" service: type: ClusterIP diff --git a/internal/embed/networks/aztec/templates/agent-rbac.yaml b/internal/embed/networks/aztec/templates/agent-rbac.yaml new file mode 100644 index 0000000..5b330b0 --- /dev/null +++ b/internal/embed/networks/aztec/templates/agent-rbac.yaml @@ -0,0 +1,17 @@ +# Grant Obol Agent admin access to this namespace +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: obol-agent-access + # Namespace is set by Helm release + labels: + app.kubernetes.io/part-of: obol.stack + obol.stack/id: {{ .Values.id }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: admin +subjects: + - kind: ServiceAccount + name: obol-agent + namespace: agent \ No newline at end of file diff --git a/internal/embed/networks/ethereum/helmfile.yaml.gotmpl b/internal/embed/networks/ethereum/helmfile.yaml.gotmpl index e363c11..555e5f3 100644 --- a/internal/embed/networks/ethereum/helmfile.yaml.gotmpl +++ b/internal/embed/networks/ethereum/helmfile.yaml.gotmpl @@ -35,7 +35,6 @@ releases: addresses: mainnet: https://mainnet-checkpoint-sync.attestant.io sepolia: https://checkpoint-sync.sepolia.ethpandaops.io - holesky: https://checkpoint-sync.holesky.ethpandaops.io hoodi: https://checkpoint-sync.hoodi.ethpandaops.io # Execution client diff --git a/internal/embed/networks/ethereum/templates/agent-rbac.yaml b/internal/embed/networks/ethereum/templates/agent-rbac.yaml new file mode 100644 index 0000000..5b330b0 --- /dev/null +++ b/internal/embed/networks/ethereum/templates/agent-rbac.yaml @@ -0,0 +1,17 @@ +# Grant Obol Agent admin access to this namespace +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: obol-agent-access + # Namespace is set by Helm release + labels: + app.kubernetes.io/part-of: obol.stack + obol.stack/id: {{ .Values.id }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: admin +subjects: + - kind: ServiceAccount + name: obol-agent + namespace: agent \ No newline at end of file diff --git a/internal/embed/networks/ethereum/values.yaml.gotmpl b/internal/embed/networks/ethereum/values.yaml.gotmpl index c119259..874a0e2 100644 --- a/internal/embed/networks/ethereum/values.yaml.gotmpl +++ b/internal/embed/networks/ethereum/values.yaml.gotmpl @@ -1,7 +1,7 @@ # Configuration via CLI flags # Template fields populated by obol CLI during network installation -# @enum mainnet,sepolia,holesky,hoodi +# @enum mainnet,sepolia,hoodi # @default mainnet # @description Blockchain network to deploy network: {{.Network}} diff --git a/internal/embed/networks/helios/helmfile.yaml.gotmpl b/internal/embed/networks/helios/helmfile.yaml.gotmpl index a77ef0b..2be4293 100644 --- a/internal/embed/networks/helios/helmfile.yaml.gotmpl +++ b/internal/embed/networks/helios/helmfile.yaml.gotmpl @@ -66,3 +66,23 @@ releases: } } } + + # Grant Obol Agent access + - name: helios-agent-access + namespace: helios-{{ .Values.id }} + chart: bedag/raw + values: + - resources: + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + name: obol-agent-access + namespace: helios-{{ .Values.id }} + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: admin + subjects: + - kind: ServiceAccount + name: obol-agent + namespace: agent diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..6932b83 --- /dev/null +++ b/renovate.json @@ -0,0 +1,81 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended" + ], + "enabledManagers": [ + "custom.regex", + "github-actions" + ], + "customManagers": [ + { + "customType": "regex", + "description": "Update obol-stack-front-end version from GitHub releases", + "matchStrings": [ + "tag:\\s*[\"'](?v[0-9]+\\.[0-9]+\\.[0-9]+)[\"']" + ], + "fileMatch": [ + "^internal/embed/infrastructure/values/obol-frontend\\.yaml\\.gotmpl$" + ], + "datasourceTemplate": "github-releases", + "depNameTemplate": "ObolNetwork/obol-stack-front-end", + "versioningTemplate": "semver" + } + ], + "packageRules": [ + { + "matchManagers": [ + "github-actions" + ], + "matchDepTypes": [ + "github-actions" + ], + "matchFileNames": [ + ".github/workflows/**" + ], + "schedule": [ + "every hour" + ], + "labels": [ + "renovate/github-actions" + ], + "groupName": "GitHub Actions updates" + }, + { + "description": "Group obol-stack-front-end updates", + "matchDatasources": [ + "github-releases" + ], + "matchPackageNames": [ + "ObolNetwork/obol-stack-front-end" + ], + "labels": [ + "renovate/frontend", + "obol-stack-front-end" + ], + "schedule": [ + "every hour" + ], + "groupName": "obol-stack-front-end updates", + "prBodyTemplate": "This PR updates **obol-stack-front-end** to version {{newVersion}}.\n\n### What Changed\n- **Current Version**: `{{currentVersion}}`\n- **New Version**: `{{newVersion}}`\n- **Change Type**: {{#if isMajor}}🔴 Major{{else}}{{#if isMinor}}🟡 Minor{{else}}🟢 Patch{{/if}}{{/if}}\n\n### Release Notes\n\n{{{changelog}}}\n\n### Files Updated\n{{#each upgrades}}- `{{depName}}`: `{{currentVersion}}` → `{{newVersion}}`\n{{/each}}\n\n---\n**Auto-generated by Renovate Bot**" + }, + { + "description": "Require approval for major obol-stack-front-end updates", + "matchDatasources": [ + "github-releases" + ], + "matchPackageNames": [ + "ObolNetwork/obol-stack-front-end" + ], + "matchUpdateTypes": [ + "major" + ], + "labels": [ + "renovate/major-update", + "requires-review" + ], + "dependencyDashboardApproval": true, + "prBodyTemplate": "⚠️ **MAJOR VERSION UPDATE** ⚠️\n\nThis PR updates **obol-stack-front-end** from `{{currentVersion}}` to `{{newVersion}}`.\n\n### ⚠️ Breaking Changes Expected\n\nMajor version updates may include breaking changes. Please review the release notes carefully.\n\n### Release Notes\n\n{{{changelog}}}\n\n### Migration Checklist\n- [ ] Review breaking changes in release notes\n- [ ] Test the new version in staging environment\n- [ ] Update any integration code if needed\n- [ ] Verify deployment scripts still work\n\n---\n**⚠️ This PR requires manual approval due to major version change**\n**Auto-generated by Renovate Bot**" + } + ] +}