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
134 changes: 134 additions & 0 deletions contrib/ACCESS_SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Centaur access setup — Grafana, GKE, sqd-compute-cluster, GCP logs, Linear

Wiring so **centaur agents** (not you locally) can reach these five systems.
Two of them ride existing tools (`grafana`, `linear`); three are new read-only
tools shipped in this fork (`k8s`, `gcp-logs`). Secrets are resolved by iron-proxy
from 1Password as `op://<vault>/<NAME>/credential`.

- **1Password vault:** `nlk6gbcqu6ddb43laiodtuntta`
- **Every secret item:** title = the secret name, one field literally named
`credential`.

The code side (this fork) is already done:
- `overlays.sources` adds `subsquid/centaur` on top of upstream (`values.mo4islona.yaml`).
- `sandbox.extraEnv` injects `GRAFANA_URL`, the `KUBE_*` cluster config, and
`GCP_LOGGING_PROJECT` (`values.mo4islona.yaml`); the chart merges these into the
sandbox env without dropping the overlay skill dirs.
- `tools/infra/grafana` host allowlist now includes `grafana.infra.gc.subsquid.io`.
- `tools/infra/k8s` and `tools/infra/gcp-logs` are the new read-only tools.
- `contrib/k8s-readonly/readonly-sa.yaml` is the per-cluster RBAC.

What remains is the parts only you can do: create the tokens, drop them into
1Password, apply RBAC, and roll the deploy.

---

## 0. Prereq — repo-cache must read the private fork

The overlay source `subsquid/centaur` is private, so repo-cache needs a GitHub
token with read access. Either seed `GITHUB_TOKEN` via the bootstrap script or
set `repoCache.githubToken.existingSecretName`. Without it the overlay silently
falls back to upstream-only and the new tools never appear.

Also **push this branch** (`feat/per-user-tenancy`) to `subsquid/centaur` — the
`overlays.sources` ref points at it. (Bump the ref once it merges to `main`.)

---

## 1. Linear (existing tool — secret only)

1. Get a Linear API key (Linear → Settings → API → Personal API keys), scoped
read/write as you want the agent to act.
2. Store it:
```bash
op item create --vault nlk6gbcqu6ddb43laiodtuntta --category "API Credential" \
--title LINEAR_API_KEY credential="lin_api_xxx"
```

## 2. Grafana (existing tool — self-hosted, host override already in the fork)

1. In `https://grafana.infra.gc.subsquid.io` → Administration → Service accounts,
create a **Viewer** service account and a token.
2. Store it:
```bash
op item create --vault nlk6gbcqu6ddb43laiodtuntta --category "API Credential" \
--title GRAFANA_API_KEY credential="glsa_xxx"
```
`GRAFANA_URL` is already injected by the chart (not a secret).

## 3. GKE + sqd-compute-cluster (new `k8s` tool)

Read-only SA bound to the built-in `view` ClusterRole (excludes Secrets).

```bash
# Apply RBAC in BOTH clusters
kubectl --context gke_bright-meridian-316511_europe-west3_main \
-f contrib/k8s-readonly/readonly-sa.yaml apply
kubectl --context sqd-compute-cluster \
-f contrib/k8s-readonly/readonly-sa.yaml apply

# Extract each token and store it
GKE_TOKEN=$(kubectl --context gke_bright-meridian-316511_europe-west3_main \
-n kube-system get secret centaur-readonly-token -o jsonpath='{.data.token}' | base64 -d)
op item create --vault nlk6gbcqu6ddb43laiodtuntta --category "API Credential" \
--title K8S_GKE_TOKEN credential="$GKE_TOKEN"

SQD_TOKEN=$(kubectl --context sqd-compute-cluster \
-n kube-system get secret centaur-readonly-token -o jsonpath='{.data.token}' | base64 -d)
op item create --vault nlk6gbcqu6ddb43laiodtuntta --category "API Credential" \
--title K8S_SQD_TOKEN credential="$SQD_TOKEN"
```

> **Reachability to verify after deploy:** the GKE API server may have *master
> authorized networks* that reject the sandbox egress IP, and the sqd endpoint is
> exposed on `:6443` — confirm the iron-proxy host rule matches host **and** port
> (adjust the `162.19.107.87:6443` entry in `tools/infra/k8s/pyproject.toml`
> `[tool.centaur].hosts` if a CONNECT to sqd is denied).

## 4. GCP logs (new `gcp-logs` tool)

iron-proxy's `gcp_auth` transform mints the access token from a SA key — the tool
carries no creds.

```bash
PROJECT=bright-meridian-316511
gcloud iam service-accounts create centaur-logs-reader \
--project "$PROJECT" --display-name "Centaur read-only logs"
gcloud projects add-iam-policy-binding "$PROJECT" \
--member "serviceAccount:centaur-logs-reader@${PROJECT}.iam.gserviceaccount.com" \
--role roles/logging.viewer
gcloud iam service-accounts keys create /tmp/centaur-logs-sa.json \
--iam-account "centaur-logs-reader@${PROJECT}.iam.gserviceaccount.com"

# Store the WHOLE JSON key as the credential
op item create --vault nlk6gbcqu6ddb43laiodtuntta --category "API Credential" \
--title GCP_LOGGING_SA credential="$(cat /tmp/centaur-logs-sa.json)"
rm -f /tmp/centaur-logs-sa.json
```

---

## 5. Roll the deploy

```bash
helm upgrade --install centaur contrib/chart -n centaur --create-namespace \
-f contrib/chart/values.dev.yaml \
-f contrib/chart/values.mo4islona.yaml \
-f contrib/chart/values.mo4islona-pool.yaml
```

## 6. Verify from an agent

```
call tools # k8s, gcp-logs, grafana, linear all listed
call discover k8s
call k8s clusters
call k8s pods gke centaur
call gcp-logs read 'resource.type="k8s_container"' --freshness 30m
call grafana search
call linear ...
```

If a tool is missing: check repo-cache synced the overlay (Prereq 0). If a call
gets a 401/405 through the proxy: the host isn't in the tool's allowlist or the
1Password item name/field is wrong.
32 changes: 32 additions & 0 deletions contrib/chart/values.mo4islona.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,22 @@ sandbox:
image:
repository: docker.io/mo4islona/centaur-agent
pullPolicy: Always
# Non-secret tool config injected into every sandbox. The chart merges these
# into SESSION_SANDBOX_EXTRA_ENV alongside the overlay skill dirs (it does NOT
# clobber them — set them here, not in apiRs.extraEnv). Bearer tokens are NOT
# here: those come from iron-proxy / 1Password. These are plain values the tool
# clients read via os.getenv.
extraEnv:
# grafana tool (self-hosted base URL)
GRAFANA_URL: https://grafana.infra.gc.subsquid.io
# k8s tool: per-cluster API endpoints + labels (CA bundles ship in the tool)
KUBE_CLUSTERS: gke,sqd
KUBE_GKE_LABEL: GKE main (bright-meridian-316511 / europe-west3)
KUBE_GKE_SERVER: https://35.246.168.135
KUBE_SQD_LABEL: sqd-compute-cluster (self-managed)
KUBE_SQD_SERVER: https://162.19.107.87:6443
# gcp-logs tool: default project
GCP_LOGGING_PROJECT: bright-meridian-316511

ironProxy:
image:
Expand All @@ -37,3 +53,19 @@ console:
image:
repository: docker.io/mo4islona/centaur-console
pullPolicy: Always

# Tool delivery. Base tools come from upstream paradigmxyz/centaur; this fork is
# layered on top as an overlay so its infra tools (k8s, gcp-logs) and its
# self-hosted grafana host override SHADOW the upstream copies (later source
# wins on name collision). repo-cache syncs both.
#
# PREREQ: subsquid/centaur is private, so repo-cache needs a GitHub token with
# read access — set repoCache.githubToken.existingSecretName (or seed GITHUB_TOKEN
# via bootstrap-k8s-secrets.sh). `ref` must point at a branch/tag that carries
# tools/infra/{k8s,gcp-logs} + the grafana host override (this branch until merged).
overlays:
sources:
- repo: paradigmxyz/centaur
ref: ""
- repo: subsquid/centaur
ref: feat/per-user-tenancy
51 changes: 51 additions & 0 deletions contrib/k8s-readonly/readonly-sa.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Read-only ServiceAccount for the centaur `k8s` tool.
#
# Apply once per cluster (GKE main + sqd-compute-cluster). Binds a dedicated SA
# to the built-in `view` ClusterRole (read-only; notably excludes Secrets), then
# mints a long-lived token the iron-proxy injects as the cluster's bearer.
#
# kubectl --context gke_bright-meridian-316511_europe-west3_main -f contrib/k8s-readonly/readonly-sa.yaml apply
# kubectl --context sqd-compute-cluster -f contrib/k8s-readonly/readonly-sa.yaml apply
#
# Then extract the token (goes into 1Password as K8S_GKE_TOKEN / K8S_SQD_TOKEN):
# kubectl --context <ctx> -n kube-system get secret centaur-readonly-token \
# -o jsonpath='{.data.token}' | base64 -d
apiVersion: v1
kind: ServiceAccount
metadata:
name: centaur-readonly
namespace: kube-system
labels:
app.kubernetes.io/managed-by: centaur
app.kubernetes.io/part-of: centaur-tools
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: centaur-readonly-view
labels:
app.kubernetes.io/managed-by: centaur
app.kubernetes.io/part-of: centaur-tools
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: view
subjects:
- kind: ServiceAccount
name: centaur-readonly
namespace: kube-system
---
# Long-lived (non-expiring) token for the SA. Modern clusters issue short-lived
# tokens via TokenRequest; this legacy Secret form gives a static bearer suitable
# for storing in 1Password. Rotate by deleting + re-creating this Secret.
apiVersion: v1
kind: Secret
metadata:
name: centaur-readonly-token
namespace: kube-system
annotations:
kubernetes.io/service-account.name: centaur-readonly
labels:
app.kubernetes.io/managed-by: centaur
app.kubernetes.io/part-of: centaur-tools
type: kubernetes.io/service-account-token
6 changes: 6 additions & 0 deletions tools/infra/gcp-logs/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Local dev for the gcp-logs tool. In production the access token is minted by
# iron-proxy's gcp_auth transform from the GCP_LOGGING_SA key in 1Password.
GCP_LOGGING_PROJECT=bright-meridian-316511
# Local-only: path to a service-account key JSON to auth directly (prod uses the
# proxy). Not read by the tool itself — use with `gcloud auth` locally.
# GOOGLE_APPLICATION_CREDENTIALS=/path/to/sa.json
68 changes: 68 additions & 0 deletions tools/infra/gcp-logs/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""CLI for read-only Google Cloud Logging."""

import json

import typer
from dotenv import load_dotenv
from rich.console import Console

from centaur_sdk import Table

load_dotenv()

app = typer.Typer(name="gcp-logs", help="Read-only Google Cloud Logging queries")
console = Console()


def get_client(project: str | None = None):
from .client import GcpLogsClient

return GcpLogsClient(project)


@app.command("read")
def read(
filter_expr: str = typer.Argument(None, help="Cloud Logging filter expression"),
freshness: str = typer.Option("1h", "--freshness", "-f", help="Time window, e.g. 30m, 1h, 2d"),
limit: int = typer.Option(50, "--limit", "-n", help="Max entries"),
order: str = typer.Option("desc", "--order", help="Sort by timestamp: asc | desc"),
project: str = typer.Option(None, "--project", "-p", help="GCP project id"),
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
):
"""Query log entries. Example filter: 'resource.type="k8s_container" AND severity>=ERROR'."""
rows = get_client(project).read(filter_expr, freshness, limit, order)
if json_output:
print(json.dumps(rows, indent=2, default=str))
return
if not rows:
console.print("[yellow]No log entries[/]")
return
table = Table(title="Log entries")
for col in ("Timestamp", "Severity", "Resource", "Log", "Payload"):
table.add_column(col)
for r in rows:
payload = r["payload"]
if not isinstance(payload, str):
payload = json.dumps(payload, default=str)
table.add_row(
str(r["timestamp"]),
str(r["severity"]),
str(r["resource"]),
str(r["log"]),
payload[:200],
)
console.print(table)


@app.command("logs")
def logs(
limit: int = typer.Option(200, "--limit", "-n", help="Max log names"),
project: str = typer.Option(None, "--project", "-p", help="GCP project id"),
):
"""List log names present in the project."""
for name in get_client(project).logs(limit):
print(name)


if __name__ == "__main__":
app()
Loading
Loading