g9s

module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 29, 2026 License: MIT

README

g9s

Latest release CI Downloads

A k9s-style terminal UI for Google Cloud. Pick a project, browse what's running in it, act on it — without leaving the terminal or juggling gcloud config set project.

Built for the case where you have several projects, each reached through a different account, and those accounts expire daily.

⬇️ Download

No Go toolchain, no dependencies compiled on your machine. These links always resolve to the newest release:

Platform Download
macOS — Apple Silicon (M1–M4) g9s_darwin_arm64.tar.gz
macOS — Intel g9s_darwin_amd64.tar.gz
Linux — x86-64 g9s_linux_amd64.tar.gz
Linux — ARM64 g9s_linux_arm64.tar.gz
checksums.txt

📋 All releases and version history → — every version, with auto-generated notes of what changed.

One-liner for Apple Silicon:

curl -L https://github.com/TTMathCS/g9s/releases/latest/download/g9s_darwin_arm64.tar.gz | tar xz --strip-components=1 && sudo mv g9s /usr/local/bin/ && g9s -version

Full instructions — checksum and signed-provenance verification, other platforms, the macOS Gatekeeper step, and building from source instead — are under Install. gcloud is still required separately; see Requirements.

Three screens, in the order you move through them.

Projects. You open here, and the thing you actually need to know is already on screen: which of your ten projects you can use right now. Green is good for another 38 minutes, amber expired overnight, hollow means this machine has never logged in. Press l on any row and gcloud takes the terminal to fix it.

Dashboard. Selecting a project fans out across every resource kind at once and lands you here — what exists, how much of it, and what state it is in, before you drill into anything. A category whose listing came back partial says so on its own row, so a truncated list never reads as an empty one. enter opens the category under the cursor, or jump straight in with 1/2/3.

Resources. The table for one category, colour-coded by status. a swaps to All Resources, which merges every kind into one table keyed by kind, name, location and status — the flat "what is in this project" list. esc goes back up to the dashboard, p all the way out to the project list.

All three screenshots are generated from the real rendering code — see docs/. The projects, IDs and accounts in them are invented.

Roadmap

Legend: ✅ shipped · 🔜 next up · 💡 candidate · ⛔ not planned (by design, not an oversight)

Resource kinds
Kind Status Scope Notes
Compute Engine instances ✅ zonal, aggregated one aggregatedList call covers every zone
GKE clusters ✅ zonal + regional, aggregated parent: projects/*/locations/- covers everything in one call
Cloud Storage buckets ✅ global simplest lister — one call, no fan-out
Dataproc clusters ✅ regional a client per region; global always swept
Cloud Composer environments ✅ location-scoped one client, location in the request parent
BigQuery datasets & recent jobs 🔜 global jobs answer "what is running", not just "what exists"
Cloud SQL instances 🔜 global version, tier, HA state, maintenance window
Pub/Sub topics & subscriptions 🔜 global subscription backlog is the number people actually want
Secret Manager secrets 🔜 global names and versions only — never values
Cloud Run services & jobs 🔜 regional
Dataproc jobs 🔜 regional clusters without jobs is half the story
Dataflow jobs 🔜 regional
Service accounts & keys 🔜 global key age is a standing audit question
GKE node pools 🔜 per-cluster drill-down from a GKE row, not a new top-level tab
Compute/serverless (Functions, Batch, instance groups, disks, GPU/TPU) 💡 mixed
Data (Bigtable, Spanner, Memorystore, Firestore, Datastream, Artifact Registry) 💡 mixed
Networking (VPC, firewall, LB, Cloud DNS, VPN, Interconnect, PSC) 💡 mixed
Security/identity (IAM bindings, KMS, Certificate Manager, VPC-SC, Org Policy) 💡 mixed
Operations (Logging, Monitoring alerts, Error Reporting, Scheduler, Cloud Build) 💡 mixed
Cost & quota (usage vs. limits, monthly spend) 💡 mixed needs billing export reachable
Platform features
Feature Status Notes
Per-project dashboard with status rollups ✅
Merged All Resources view across kinds ✅
Filter, describe-as-YAML, Console/Airflow links, OSC 52 yank, SSH ✅
Mutating actions behind a confirmation 🔜 VM / Dataproc power state first — no Terraform drift
Terraform state overlay (managed / drifted / unmanaged) 🔜 the single most useful thing on this list, and the most work
Cloud Asset Inventory fast path 💡 optional — plenty of orgs don't enable the API
Cross-project view (one kind, every project at once) 💡 the other axis from the dashboard's per-kind rollup
Saved filters / bookmarks 💡
Export current table to CSV/JSON 💡
Prebuilt release binaries (no Go toolchain needed) ✅ macOS + Linux, arm64 + amd64, with checksums and signed SLSA provenance — see Install
Writing infrastructure ⛔ not a Terraform replacement
Storing credentials ⛔ gcloud owns that; g9s never touches a credential
Displaying secret values ⛔ names/versions only — use gcloud secrets versions access, which is logged

ROADMAP.md has the full picture with reasoning per item — why each is scoped the way it is, and why global/regional/zonal is what decides the cost of adding it.

Why this exists

Cloud Asset Inventory makes "list everything in a project" a single API call. Without it — and plenty of orgs don't enable it — you fan out across a dozen service APIs, several of which are region-scoped, and you do it again for every project. g9s does that fan-out and puts the result in one keyboard-driven table.

Status

MVP. Five resource kinds — Compute Engine, GKE, Cloud Storage, Dataproc, Cloud Composer — read-only plus SSH. The resource layer is behind a one-method interface, so adding a kind is one new file — see Adding a resource kind.

Navigation is three levels deep: projects → dashboard → a category's table, with esc walking back up. A new kind appears on the dashboard, in the tab bar and in All Resources automatically; there is nothing to register in the UI.

The number keys reach kinds 1–9, so there is room for four more before the digits run out; past that, tab/shift+tab, 0/a and the : commands still reach everything.

Requirements

gcloud CLI — required, not optional. g9s checks for it at startup and exits with gcloud not found rather than letting you discover the problem mid-session. It's needed because login and SSH are the two places a human is involved:

  • l runs gcloud auth application-default login to mint credentials
  • s runs gcloud compute ssh

Everything else — the resource listing — talks to the GCP APIs directly and never shells out. See Design notes for why.

Go 1.25+ — only if you build from source. Not needed if you download a release binary, which is self-contained. Go is never a runtime dependency.

Install

Two options. The first needs no Go toolchain and pulls no dependencies onto your machine; the second builds from source.

Option 1: download a release binary (no Go toolchain)

Every tagged release attaches archives for macOS and Linux on both Apple Silicon/ARM and Intel/AMD64, plus a checksums.txt. Archive names carry no version, so releases/latest/download/… is a permanent URL — the version lives in the release tag, the directory inside the archive, and g9s -version.

# Pick your platform: darwin_arm64, darwin_amd64, linux_amd64, linux_arm64
PLATFORM=darwin_arm64
BASE=https://github.com/TTMathCS/g9s/releases/latest/download

curl -LO "${BASE}/g9s_${PLATFORM}.tar.gz"
curl -LO "${BASE}/checksums.txt"

# Verify before extracting, not after
shasum -a 256 -c checksums.txt --ignore-missing

tar -xzf "g9s_${PLATFORM}.tar.gz" --strip-components=1
sudo mv g9s /usr/local/bin/
g9s -version

To pin a specific version instead of tracking the latest, swap latest/download for download/v0.1.0 — see all releases.

Verifying provenance, not just integrity. The checksum only proves your download wasn't corrupted in transit — it says nothing about where the file came from. Each archive also carries a signed SLSA build provenance attestation tying it to this repository, the exact commit and the workflow run that produced it. If you have the gh CLI:

gh attestation verify "g9s_${PLATFORM}.tar.gz" --repo TTMathCS/g9s

That is the check worth running. It fails if the archive was built anywhere other than this repo's CI.

On macOS, Gatekeeper will complain the first time — the binaries are not Apple-notarised (that needs a paid Developer ID). Clear it with xattr -d com.apple.quarantine /usr/local/bin/g9s, or right-click → Open once. If you would rather not, build from source instead.

Option 2: build from source

Needs Go 1.25+ and fetches roughly a hundred module dependencies. See Setup on a new Mac below for the full toolchain walkthrough, or if you already have Go:

go install github.com/TTMathCS/g9s/cmd/g9s@latest

Setup on a new Mac

For building from source. Both paths work on Apple Silicon and Intel. Pick one.

With Homebrew
brew install go
brew install --cask gcloud-cli      # the cask was renamed from google-cloud-sdk

The cask symlinks gcloud into your Homebrew prefix (/opt/homebrew/bin on Apple Silicon, /usr/local/bin on Intel), so it lands on your PATH with no further setup.

If Homebrew itself isn't installed yet and you want it:

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

On Apple Silicon its installer prints two eval lines to add /opt/homebrew/bin to your PATH — run them, or brew won't be found in new shells.

Without Homebrew

Go — download the macOS .pkg from go.dev/dl (ARM64 for Apple Silicon, x86-64 for Intel) and run it. It installs to /usr/local/go and adds /usr/local/go/bin to your PATH via /etc/paths.d/go, which takes effect in new shells.

gcloud — download and run Google's installer:

# Apple Silicon; for Intel swap darwin-arm for darwin-x86_64
curl -O https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-cli-darwin-arm.tar.gz
tar -xf google-cloud-cli-darwin-arm.tar.gz
./google-cloud-sdk/install.sh

install.sh offers to edit your shell profile to add gcloud to PATH — say yes, or you'll have to invoke it by full path. Extract it somewhere permanent (~/google-cloud-sdk is conventional); the install location is the installation, and moving it later breaks the profile entry.

Skip gcloud init. It configures a default project in your global gcloud state, which g9s deliberately doesn't use — it sets CLOUDSDK_CONFIG per project instead. Running it is harmless, just pointless here.

gcloud needs Python 3. macOS provides it with the Xcode Command Line Tools (xcode-select --install); if gcloud picks the wrong interpreter, point it at one with CLOUDSDK_PYTHON=/path/to/python3.

Then install g9s

Two ways. Use the second if your network doesn't reach proxy.golang.org.

A — go install:

go install github.com/TTMathCS/g9s/cmd/g9s@latest

This drops the binary in $(go env GOPATH)/bin, normally ~/go/bin — not on your PATH by default on a fresh Mac. Add it:

echo 'export PATH="$HOME/go/bin:$PATH"' >> ~/.zshrc
exec zsh

(macOS has used zsh as the default shell since Catalina. On bash, use ~/.bash_profile.)

B — clone and build:

git clone https://github.com/TTMathCS/g9s.git
cd g9s
go build -o g9s ./cmd/g9s     # binary lands in the current directory

Copy the binary onto your PATH yourself (sudo mv g9s /usr/local/bin/), or run it as ./g9s.

Behind a corporate proxy

Point GOPROXY at your internal Go registry. For Artifactory the api/go path segment is required:

go env -w GOPROXY="https://artifactory.example.com/artifactory/api/go/<go-repo>"
go env -w GOSUMDB=off    # unless your registry proxies sum.golang.org

go env -w persists these to ~/.config/go/env, so they survive new shells and don't need to live in your profile.

Authenticate with ~/.netrc — Go reads it natively, which keeps the token out of GOPROXY and out of your shell history:

machine artifactory.example.com
login <username>
password <api-key-or-access-token>
chmod 600 ~/.netrc

With that in place both install methods work normally. If you use the JFrog CLI, jf go-config followed by jf go build ./cmd/g9s sets GOPROXY for you.

Which method to use. Cloning does not remove the need for a module registry — go build still resolves ~30 dependencies, so a clone alone doesn't get you an offline build. Method B helps in the specific case where your registry serves common dependencies but won't resolve github.com/TTMathCS/g9s itself — a brand-new repo that isn't cached, or a registry with an approval allowlist. The clone sidesteps that one lookup; the dependencies still come from Artifactory.

Toolchain gotcha. go.mod requires Go 1.25.0, and the default GOTOOLCHAIN=auto will try to download a matching toolchain through GOPROXY if your local Go is older. On a restricted network that fails with a confusing error. Install Go 1.25+ directly and it never comes up; go env -w GOTOOLCHAIN=local makes the attempt fail fast and loudly instead.

Verify
gcloud --version     # any recent version
go version           # must be 1.25 or newer
g9s -version         # prints "g9s dev" — the version is only stamped in tagged release builds

If g9s isn't found, the PATH line above is what's missing. If g9s starts and immediately prints gcloud not found at "gcloud", gcloud isn't on the PATH of the shell you launched it from — open a new terminal, since profile edits don't apply retroactively.

Quick start

g9s -init          # writes ~/.config/g9s/config.yaml
$EDITOR ~/.config/g9s/config.yaml
g9s

Start with a single project and add the rest once it works.

On first launch every project shows ○ not logged in. Select one, press l, and gcloud takes over the terminal to run the login. Once it's done you're dropped back into the table.

Nothing needs to be logged in ahead of time, and you don't need to have run gcloud on this machine before — g9s keeps its credentials in its own directory and never reads or writes your global ~/.config/gcloud. Moving to a new Mac therefore means logging in again; there's no credential state worth copying across, and copying it would defeat the isolation.

Staying current
go install github.com/TTMathCS/g9s/cmd/g9s@latest   # upgrade g9s
brew upgrade --cask gcloud-cli                      # or: gcloud components update

Use gcloud components update for the non-Homebrew install. It doesn't work on the Homebrew cask — brew owns those files and gcloud will tell you so.

How authentication works

This is the part worth understanding, because it's usually mis-modelled.

g9s never sees your password. When you press l, it suspends itself and runs gcloud auth application-default login with the terminal handed over. gcloud opens your browser; your identity provider's login page — including the password from your PAM checkout and the MFA challenge — is handled entirely by the browser. g9s resumes once gcloud has written the credentials to disk.

If your identity is federated (Entra ID, Okta, or similar), that browser redirect is what carries you to your IdP. There is no way to type an SSO password into a terminal and have Google accept it, and you should be suspicious of any tool that offers to.

Each project gets its own credential directory. g9s sets CLOUDSDK_CONFIG to a per-project path under credential_dir, so:

  • logging into one project never disturbs another,
  • ten projects with ten different support accounts coexist without gcloud config configurations juggling,
  • nothing g9s does mutates your normal ~/.config/gcloud state.

Expiry is detected by using the credentials, not by reading a timestamp. A refresh token your IdP has invalidated looks perfectly healthy on disk. g9s mints a real access token to check, so an expired session shows up as ● expired — press l to re-login rather than as a confusing API error ten seconds later. With a typical federated session policy you should expect to re-login roughly once a day.

When the terminal isn't on the machine with the browser

Press L instead of l. That adds --no-browser, which prints a bootstrap command to run on a trusted machine that has both a browser and gcloud; you paste the resulting URL back. It's fiddlier than the local flow, so prefer running g9s on your workstation if you can.

Configuration

~/.config/g9s/config.yaml, or $G9S_CONFIG, or -config <path>.

defaults:
  # Swept for region-scoped resources unless a project overrides them.
  # Keep this tight: every region is another API call on every refresh.
  regions:
    - northamerica-northeast1
    - us-central1

  credential_dir: ~/.local/share/g9s/credentials
  gcloud_path: gcloud
  list_timeout: 90s

projects:
  - name: sandbox                    # label in the picker; names the credential dir
    project_id: my-sandbox-project
    description: personal access, read-only

  - name: prod-data
    project_id: my-prod-data-project
    account: svc-prod-support@example.com   # passed to gcloud --account
    regions:
      - northamerica-northeast1             # overrides defaults.regions
    composer_locations:
      - us-central1                         # overrides both, for Composer only

Region resolution runs most-specific-first: projects[].composer_locations → projects[].regions → defaults.composer_locations → defaults.regions. Dataproc works the same way via dataproc_regions, and always includes the global region, which is easy to forget and does hold clusters.

Unknown keys are an error rather than a silent default, so a typo'd regionz: tells you instead of quietly scanning nothing.

Keys

The bindings follow k9s muscle memory where the two tools overlap: : jumps by name, d describes, s opens a shell (here: SSH), / filters, and q/esc back out one level rather than quitting.

Key Action
↑/k, ↓/j move cursor
g / G top / bottom
enter dashboard: open the category · table: describe (YAML, as gcloud describe shows it)
d describe the selected resource
: command — :vm :gke :gcs :dataproc :composer :all :projects :q (prefixes work: :data)
1–9 jump straight to a resource kind
0 / a all resources — every kind in one table
tab / shift+tab cycle resource kinds
q / esc back up one level — table to dashboard, dashboard to projects
p back to the project list
/ filter rows; esc clears
r refresh current kind — every kind when on the dashboard
o open — Airflow UI for Composer, Cloud Console otherwise
c open in Cloud Console
y copy name to clipboard (OSC 52, works over SSH)
s SSH to the selected running VM
l / L log in / log in without a local browser
? help
ctrl+c / :q quit — or q from the project list

Partial results are shown as partial

With a least-privilege account, some regions and some APIs will refuse you. A tool that discards the whole refresh because one of ten regions returned 403 is useless, and one that silently drops it is worse — an empty table reads as "nothing is running here."

So listers return whatever succeeded plus a warning per failed scope, and the footer says so:

⚠ 2 scope(s) unavailable: europe-west1: permission denied; us-east4: permission denied

Errors that are expected rather than informative — the API simply isn't enabled in that region, or the region doesn't exist — are suppressed, or the footer would be permanently full of noise.

Adding a resource kind

Implement gcp.Lister in a new file under internal/gcp and add it to Listers():

type Lister interface {
	Kind() Kind
	List(ctx context.Context, cfg *config.Config, p config.Project, opts []option.ClientOption) (Result, error)
}

Use fanOut for anything region-scoped; it handles the concurrency, the partial-failure collection and the stable ordering. internal/gcp/dataproc.go is the shortest example.

Two things the table relies on: your Resource.Row must have exactly as many cells as Kind.Columns, and the number keys only reach the first nine listers. Both are covered by tests.

Design notes

Why not shell out to gcloud ... --format=json? It's the fast way to build this and it handles auth for free, but each invocation is a ~1–2s Python cold start. Across a fan-out of a dozen regions the UI would feel dead. g9s uses gcloud only where a human is involved — login and SSH — and talks to the APIs directly everywhere else.

Why is Dataproc the awkward one? Its endpoint is regional. A request for us-central1 sent to the default endpoint returns nothing rather than an error, so each region needs its own client pointed at <region>-dataproc.googleapis.com. Composer is location-scoped through the request parent instead, so one client covers every location. Compute needs no fan-out at all — aggregatedList returns every zone in one call.

Why a quota project? Application default credentials minted from a user account have no project of their own, and most APIs reject the call outright without one attached. g9s sets it on every client.

Security

g9s never sees your password, never writes a credential, and issues no mutating API call — every request is a List or a Get. Credentials are isolated per project under a 0700 directory, and the config file is refused if anyone else can write it, since gcloud_path decides which binary gets executed.

SECURITY.md covers the threat model, what the tool can reach and run, the findings from a July 2026 code review (two issues found and fixed, plus the paths examined and cleared), and the dependency posture. CI runs govulncheck ./... on every push.

License

MIT

Directories

Path Synopsis
cmd
g9s command
Command g9s is a terminal UI for browsing Google Cloud resources.
Command g9s is a terminal UI for browsing Google Cloud resources.
internal
auth
Package auth manages per-project GCP credentials.
Package auth manages per-project GCP credentials.
config
Package config loads and validates the g9s configuration file.
Package config loads and validates the g9s configuration file.
gcp
Package gcp lists GCP resources for a project.
Package gcp lists GCP resources for a project.
ui

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL