Rebrand: Sentry -> Cairn OBS
Full rebrand across cosmetic branding, code identifiers, and infrastructure/data-plane naming, using the supplied Cairn OBS logo package. Cosmetic: favicon/logo swap (also closes a stale license-audit finding -- the old favicon was SvelteKit's unreplaced scaffold logo), new centered welcome landing page, larger/legible sidebar logo, page titles, CLAUDE.md/README/docs prose. Code identifiers: Go module path github.com/sentry/sentry -> github.com/cairnobs/cairnobs across all 13 modules and ~91 files (protoc regenerated); Rust crates sentry-agent/sentry-parser/sentry-search -> cairnobs-*; CLI sentryctl -> cairnobsctl; Terraform provider fully renamed (sentry_dashboard etc. -> cairnobs_dashboard, provider type, env vars); every session/auth cookie name; agent config paths and Windows service identity. Deliberately preserved: the gRPC wire protocol's protobuf packages (sentry.logs.v1, sentry.agent.v1) and their Go import directory (proto/sentry/...) -- renaming the wire-level package would break every currently-deployed agent binary (confirmed two real hosts, including mail.inbuxa.com, are actively streaming through this exact contract) until rebuilt and redeployed in lockstep with an ingest cutover. Only the Go module path wrapping the generated code changes. Infrastructure: every docker-compose container name (root and three component-level compose files); the Helm chart (directory, Chart.yaml, named-template helpers, all templates, values.yaml image repos); Kubernetes Operator (CRD group sentry.io -> cairnobs.io, both CRD YAML files, Go identifiers, RBAC markers); the coupled enterprise/tenantcrd package. Caught and fixed real path-coupling bugs along the way: the Helm chart's search/ingest volume mounts and the dev-only-credential detection constant vs. docker-compose.yml's literal values had to move together or a security warning would have silently stopped firing. Data plane: Postgres database sentry_metadata -> cairnobs_metadata and role sentry -> cairnobs; ClickHouse database sentry -> cairnobs; Kafka topic sentry.logs.raw -> cairnobs.logs.raw and its consumer groups. Source-level defaults, docker-compose.yml, and every migrate.sh/ provision script default updated together; already-applied migration files left untouched per this repo's immutable-migration convention. Verified at every layer: all 13 Go modules build/vet/test clean, both Rust workspaces (agent, search) build/clippy/test clean, npm run check/ build clean, docker compose config validates on all four compose files. Live-verified against a real docker stack multiple times through this work, including a final fresh-volume run confirming the actual renamed Postgres database/role, ClickHouse database, and Kafka topic all work end to end with a real login and query, zero console errors.
This commit is contained in:
+4
-4
@@ -1,9 +1,9 @@
|
||||
# docker build -f cli/Dockerfile -t sentryctl cli/
|
||||
# docker build -f cli/Dockerfile -t cairnobsctl cli/
|
||||
FROM golang:1.25-alpine AS builder
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /out/sentryctl ./cmd/sentryctl
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /out/cairnobsctl ./cmd/cairnobsctl
|
||||
|
||||
FROM gcr.io/distroless/static-debian12
|
||||
COPY --from=builder /out/sentryctl /sentryctl
|
||||
ENTRYPOINT ["/sentryctl"]
|
||||
COPY --from=builder /out/cairnobsctl /cairnobsctl
|
||||
ENTRYPOINT ["/cairnobsctl"]
|
||||
|
||||
+19
-19
@@ -1,20 +1,20 @@
|
||||
# sentryctl
|
||||
# cairnobsctl
|
||||
|
||||
Sentry's control CLI.
|
||||
Cairn OBS's control CLI.
|
||||
|
||||
```sh
|
||||
sentryctl ping # checks http://localhost:8080/healthz
|
||||
sentryctl ping --api http://api.internal:8080
|
||||
SENTRYCTL_API_URL=http://api.internal:8080 sentryctl ping
|
||||
cairnobsctl ping # checks http://localhost:8080/healthz
|
||||
cairnobsctl ping --api http://api.internal:8080
|
||||
CAIRNOBSCTL_API_URL=http://api.internal:8080 cairnobsctl ping
|
||||
```
|
||||
|
||||
Exits 0 and prints `ok` if `/api`'s `/healthz` responds 200; exits 1 with an
|
||||
error on `stderr` otherwise.
|
||||
|
||||
```sh
|
||||
sentryctl query 'service=api | where status>=500 | stats count by host'
|
||||
sentryctl query 'SELECT * FROM logs LIMIT 10' --language sql
|
||||
sentryctl query 'message:"connection refused"' --json
|
||||
cairnobsctl query 'service=api | where status>=500 | stats count by host'
|
||||
cairnobsctl query 'SELECT * FROM logs LIMIT 10' --language sql
|
||||
cairnobsctl query 'message:"connection refused"' --json
|
||||
```
|
||||
|
||||
Quote the query in your shell — pipe syntax uses `|`, which your shell
|
||||
@@ -27,17 +27,17 @@ Prints a table by default (stdlib `text/tabwriter`, no new dependency);
|
||||
`--json` prints the raw `{columns, rows}` response instead.
|
||||
|
||||
```sh
|
||||
sentryctl dashboards list
|
||||
sentryctl dashboards get <id>
|
||||
sentryctl dashboards apply dashboard.json # imports a dashboard exported via the web UI's "Export JSON" button
|
||||
cairnobsctl dashboards list
|
||||
cairnobsctl dashboards get <id>
|
||||
cairnobsctl dashboards apply dashboard.json # imports a dashboard exported via the web UI's "Export JSON" button
|
||||
|
||||
sentryctl dashboards permissions list <dashboard-id>
|
||||
sentryctl dashboards permissions grant <dashboard-id> <user-id> viewer|editor
|
||||
sentryctl dashboards permissions revoke <dashboard-id> <user-id>
|
||||
cairnobsctl dashboards permissions list <dashboard-id>
|
||||
cairnobsctl dashboards permissions grant <dashboard-id> <user-id> viewer|editor
|
||||
cairnobsctl dashboards permissions revoke <dashboard-id> <user-id>
|
||||
|
||||
sentryctl alerts list
|
||||
sentryctl alerts get <id>
|
||||
sentryctl alerts apply rule.json # creates a rule from a JSON file shaped like POST /rules's body
|
||||
cairnobsctl alerts list
|
||||
cairnobsctl alerts get <id>
|
||||
cairnobsctl alerts apply rule.json # creates a rule from a JSON file shaped like POST /rules's body
|
||||
```
|
||||
|
||||
`dashboards permissions` is Phase 4's per-resource dashboard grant
|
||||
@@ -51,7 +51,7 @@ CLI bug.
|
||||
|
||||
`dashboards` talks to `/api` (`--api`, same override as `query`/`ping`).
|
||||
`alerts` talks to `/alerting`, a separate service with its own base URL
|
||||
(`--alerting-api`, or `$SENTRYCTL_ALERTING_API_URL`, default
|
||||
(`--alerting-api`, or `$CAIRNOBSCTL_ALERTING_API_URL`, default
|
||||
`http://localhost:8081`) — see `/docs/phase-3-alerting-design.md`'s
|
||||
component boundary for why alerting isn't just another `/api` route.
|
||||
`apply` in both cases sends the file's JSON as-is to the corresponding
|
||||
@@ -78,5 +78,5 @@ go test ./...
|
||||
```
|
||||
|
||||
```sh
|
||||
docker build -f Dockerfile -t sentryctl . # context is cli/, not the repo root
|
||||
docker build -f Dockerfile -t cairnobsctl . # context is cli/, not the repo root
|
||||
```
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
func TestResolveTokenFromEnv(t *testing.T) {
|
||||
env := func(k string) string {
|
||||
if k == "SENTRYCTL_TOKEN" {
|
||||
if k == "CAIRNOBSCTL_TOKEN" {
|
||||
return "secret-token"
|
||||
}
|
||||
return ""
|
||||
@@ -91,7 +91,7 @@ func TestCmdPingForwardsBearerToken(t *testing.T) {
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
t.Setenv("SENTRYCTL_TOKEN", "ping-token")
|
||||
t.Setenv("CAIRNOBSCTL_TOKEN", "ping-token")
|
||||
var stdout, stderr bytes.Buffer
|
||||
code := cmdPing([]string{"--api", srv.URL}, &stdout, &stderr)
|
||||
if code != 0 {
|
||||
@@ -111,7 +111,7 @@ func TestCmdQueryForwardsBearerToken(t *testing.T) {
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
t.Setenv("SENTRYCTL_TOKEN", "query-token")
|
||||
t.Setenv("CAIRNOBSCTL_TOKEN", "query-token")
|
||||
var stdout, stderr bytes.Buffer
|
||||
code := cmdQuery([]string{"--api", srv.URL, "service=api"}, &stdout, &stderr)
|
||||
if code != 0 {
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
|
||||
func cmdAgents(args []string, stdout, stderr io.Writer) int {
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintln(stderr, "sentryctl agents: expected a subcommand (list, get, config, restart)")
|
||||
fmt.Fprintln(stderr, "cairnobsctl agents: expected a subcommand (list, get, config, restart)")
|
||||
return 1
|
||||
}
|
||||
apiURL, rest := extractAPIFlag(args[1:], os.Getenv)
|
||||
@@ -28,19 +28,19 @@ func cmdAgents(args []string, stdout, stderr io.Writer) int {
|
||||
return httpGetJSON(apiURL, "/agents", token, stdout, stderr)
|
||||
case "get":
|
||||
if len(rest) == 0 {
|
||||
fmt.Fprintln(stderr, "sentryctl agents get: missing host")
|
||||
fmt.Fprintln(stderr, "cairnobsctl agents get: missing host")
|
||||
return 1
|
||||
}
|
||||
return httpGetJSON(apiURL, "/agents/"+rest[0], token, stdout, stderr)
|
||||
case "config":
|
||||
if len(rest) == 0 {
|
||||
fmt.Fprintln(stderr, "sentryctl agents config: expected a subcommand (get, set, clear)")
|
||||
fmt.Fprintln(stderr, "cairnobsctl agents config: expected a subcommand (get, set, clear)")
|
||||
return 1
|
||||
}
|
||||
return cmdAgentsConfig(rest, apiURL, token, stdout, stderr)
|
||||
case "restart":
|
||||
if len(rest) == 0 {
|
||||
fmt.Fprintln(stderr, "sentryctl agents restart: missing host")
|
||||
fmt.Fprintln(stderr, "cairnobsctl agents restart: missing host")
|
||||
return 1
|
||||
}
|
||||
// os.Stdin passed explicitly at this inner layer (not threaded
|
||||
@@ -49,7 +49,7 @@ func cmdAgents(args []string, stdout, stderr io.Writer) int {
|
||||
// call cmdAgentsRestart directly with a fake reader.
|
||||
return cmdAgentsRestart(rest, apiURL, token, os.Stdin, stdout, stderr)
|
||||
default:
|
||||
fmt.Fprintf(stderr, "sentryctl agents: unknown subcommand %q (want list, get, config, restart)\n", args[0])
|
||||
fmt.Fprintf(stderr, "cairnobsctl agents: unknown subcommand %q (want list, get, config, restart)\n", args[0])
|
||||
return 1
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,7 @@ func cmdAgentsConfig(args []string, apiURL, token string, stdout, stderr io.Writ
|
||||
switch sub {
|
||||
case "get":
|
||||
if len(rest) == 0 {
|
||||
fmt.Fprintln(stderr, "sentryctl agents config get: missing host")
|
||||
fmt.Fprintln(stderr, "cairnobsctl agents config get: missing host")
|
||||
return 1
|
||||
}
|
||||
// Same GET /agents/{host} as plain "get" -- an agent's reported
|
||||
@@ -69,18 +69,18 @@ func cmdAgentsConfig(args []string, apiURL, token string, stdout, stderr io.Writ
|
||||
return httpGetJSON(apiURL, "/agents/"+rest[0], token, stdout, stderr)
|
||||
case "set":
|
||||
if len(rest) == 0 {
|
||||
fmt.Fprintln(stderr, "sentryctl agents config set: missing host")
|
||||
fmt.Fprintln(stderr, "cairnobsctl agents config set: missing host")
|
||||
return 1
|
||||
}
|
||||
return cmdAgentsConfigSet(rest[0], rest[1:], apiURL, token, stdout, stderr)
|
||||
case "clear":
|
||||
if len(rest) == 0 {
|
||||
fmt.Fprintln(stderr, "sentryctl agents config clear: missing host")
|
||||
fmt.Fprintln(stderr, "cairnobsctl agents config clear: missing host")
|
||||
return 1
|
||||
}
|
||||
return httpMutateNoBody(http.MethodDelete, apiURL, "/agents/"+rest[0]+"/config", token, "", "config override cleared -- agent will run its local agent.toml again", stdout, stderr)
|
||||
default:
|
||||
fmt.Fprintf(stderr, "sentryctl agents config: unknown subcommand %q (want get, set, clear)\n", sub)
|
||||
fmt.Fprintf(stderr, "cairnobsctl agents config: unknown subcommand %q (want get, set, clear)\n", sub)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
@@ -135,71 +135,71 @@ func cmdAgentsConfigSet(host string, flagArgs []string, apiURL, token string, st
|
||||
case "--batch-max-size":
|
||||
v, ok := next()
|
||||
if !ok {
|
||||
fmt.Fprintln(stderr, "sentryctl agents config set: --batch-max-size requires a value")
|
||||
fmt.Fprintln(stderr, "cairnobsctl agents config set: --batch-max-size requires a value")
|
||||
return 1
|
||||
}
|
||||
n, err := strconv.ParseInt(v, 10, 64)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "sentryctl agents config set: invalid --batch-max-size %q: %v\n", v, err)
|
||||
fmt.Fprintf(stderr, "cairnobsctl agents config set: invalid --batch-max-size %q: %v\n", v, err)
|
||||
return 1
|
||||
}
|
||||
batchMaxSize = &n
|
||||
case "--batch-flush-interval-ms":
|
||||
v, ok := next()
|
||||
if !ok {
|
||||
fmt.Fprintln(stderr, "sentryctl agents config set: --batch-flush-interval-ms requires a value")
|
||||
fmt.Fprintln(stderr, "cairnobsctl agents config set: --batch-flush-interval-ms requires a value")
|
||||
return 1
|
||||
}
|
||||
n, err := strconv.ParseInt(v, 10, 64)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "sentryctl agents config set: invalid --batch-flush-interval-ms %q: %v\n", v, err)
|
||||
fmt.Fprintf(stderr, "cairnobsctl agents config set: invalid --batch-flush-interval-ms %q: %v\n", v, err)
|
||||
return 1
|
||||
}
|
||||
batchFlushMS = &n
|
||||
case "--heartbeat-interval-ms":
|
||||
v, ok := next()
|
||||
if !ok {
|
||||
fmt.Fprintln(stderr, "sentryctl agents config set: --heartbeat-interval-ms requires a value")
|
||||
fmt.Fprintln(stderr, "cairnobsctl agents config set: --heartbeat-interval-ms requires a value")
|
||||
return 1
|
||||
}
|
||||
n, err := strconv.ParseInt(v, 10, 64)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "sentryctl agents config set: invalid --heartbeat-interval-ms %q: %v\n", v, err)
|
||||
fmt.Fprintf(stderr, "cairnobsctl agents config set: invalid --heartbeat-interval-ms %q: %v\n", v, err)
|
||||
return 1
|
||||
}
|
||||
heartbeatMS = &n
|
||||
case "--heartbeat-enabled":
|
||||
v, ok := next()
|
||||
if !ok {
|
||||
fmt.Fprintln(stderr, "sentryctl agents config set: --heartbeat-enabled requires true or false")
|
||||
fmt.Fprintln(stderr, "cairnobsctl agents config set: --heartbeat-enabled requires true or false")
|
||||
return 1
|
||||
}
|
||||
b, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "sentryctl agents config set: invalid --heartbeat-enabled %q: %v\n", v, err)
|
||||
fmt.Fprintf(stderr, "cairnobsctl agents config set: invalid --heartbeat-enabled %q: %v\n", v, err)
|
||||
return 1
|
||||
}
|
||||
heartbeatEnabled = &b
|
||||
case "--journald-unit":
|
||||
v, ok := next()
|
||||
if !ok {
|
||||
fmt.Fprintln(stderr, "sentryctl agents config set: --journald-unit requires a value (empty string clears the filter)")
|
||||
fmt.Fprintln(stderr, "cairnobsctl agents config set: --journald-unit requires a value (empty string clears the filter)")
|
||||
return 1
|
||||
}
|
||||
journaldUnit = &v
|
||||
default:
|
||||
fmt.Fprintf(stderr, "sentryctl agents config set: unknown flag %q\n", flag)
|
||||
fmt.Fprintf(stderr, "cairnobsctl agents config set: unknown flag %q\n", flag)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
if batchMaxSize == nil && batchFlushMS == nil && heartbeatMS == nil && heartbeatEnabled == nil && journaldUnit == nil {
|
||||
fmt.Fprintln(stderr, "sentryctl agents config set: at least one of --batch-max-size, --batch-flush-interval-ms, --heartbeat-enabled, --heartbeat-interval-ms, --journald-unit is required")
|
||||
fmt.Fprintln(stderr, "cairnobsctl agents config set: at least one of --batch-max-size, --batch-flush-interval-ms, --heartbeat-enabled, --heartbeat-interval-ms, --journald-unit is required")
|
||||
return 1
|
||||
}
|
||||
|
||||
current, err := fetchAgent(apiURL, host, token)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "sentryctl agents config set: fetching current state: %v\n", err)
|
||||
fmt.Fprintf(stderr, "cairnobsctl agents config set: fetching current state: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -232,7 +232,7 @@ func cmdAgentsConfigSet(host string, flagArgs []string, apiURL, token string, st
|
||||
|
||||
body, err := json.Marshal(merged)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "sentryctl agents config set: encoding request: %v\n", err)
|
||||
fmt.Fprintf(stderr, "cairnobsctl agents config set: encoding request: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
return httpPutJSON(apiURL, "/agents/"+host+"/config", token, string(body), stdout, stderr)
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
func cmdAlerts(args []string, stdout, stderr io.Writer) int {
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintln(stderr, "sentryctl alerts: expected a subcommand (list, get, apply)")
|
||||
fmt.Fprintln(stderr, "cairnobsctl alerts: expected a subcommand (list, get, apply)")
|
||||
return 1
|
||||
}
|
||||
alertingURL, rest := extractAlertingAPIFlag(args[1:], os.Getenv)
|
||||
@@ -19,13 +19,13 @@ func cmdAlerts(args []string, stdout, stderr io.Writer) int {
|
||||
return httpGetJSON(alertingURL, "/rules", token, stdout, stderr)
|
||||
case "get":
|
||||
if len(rest) == 0 {
|
||||
fmt.Fprintln(stderr, "sentryctl alerts get: missing rule id")
|
||||
fmt.Fprintln(stderr, "cairnobsctl alerts get: missing rule id")
|
||||
return 1
|
||||
}
|
||||
return httpGetJSON(alertingURL, "/rules/"+rest[0], token, stdout, stderr)
|
||||
case "apply":
|
||||
if len(rest) == 0 {
|
||||
fmt.Fprintln(stderr, "sentryctl alerts apply: missing file path")
|
||||
fmt.Fprintln(stderr, "cairnobsctl alerts apply: missing file path")
|
||||
return 1
|
||||
}
|
||||
// POST /rules accepts the same shape it returns -- a rule
|
||||
@@ -33,7 +33,7 @@ func cmdAlerts(args []string, stdout, stderr io.Writer) int {
|
||||
// target ID) applies directly with no reshaping.
|
||||
return httpPostFileJSON(alertingURL, "/rules", token, rest[0], stdout, stderr)
|
||||
default:
|
||||
fmt.Fprintf(stderr, "sentryctl alerts: unknown subcommand %q (want list, get, apply)\n", args[0])
|
||||
fmt.Fprintf(stderr, "cairnobsctl alerts: unknown subcommand %q (want list, get, apply)\n", args[0])
|
||||
return 1
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ func TestExtractAlertingAPIFlagOverride(t *testing.T) {
|
||||
|
||||
func TestExtractAlertingAPIFlagFromEnv(t *testing.T) {
|
||||
alertingURL, _ := extractAlertingAPIFlag(nil, func(k string) string {
|
||||
if k == "SENTRYCTL_ALERTING_API_URL" {
|
||||
if k == "CAIRNOBSCTL_ALERTING_API_URL" {
|
||||
return "http://env-alerting:8081"
|
||||
}
|
||||
return ""
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
func cmdDashboards(args []string, stdout, stderr io.Writer) int {
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintln(stderr, "sentryctl dashboards: expected a subcommand (list, get, apply, permissions)")
|
||||
fmt.Fprintln(stderr, "cairnobsctl dashboards: expected a subcommand (list, get, apply, permissions)")
|
||||
return 1
|
||||
}
|
||||
apiURL, rest := extractAPIFlag(args[1:], os.Getenv)
|
||||
@@ -20,13 +20,13 @@ func cmdDashboards(args []string, stdout, stderr io.Writer) int {
|
||||
return httpGetJSON(apiURL, "/dashboards", token, stdout, stderr)
|
||||
case "get":
|
||||
if len(rest) == 0 {
|
||||
fmt.Fprintln(stderr, "sentryctl dashboards get: missing dashboard id")
|
||||
fmt.Fprintln(stderr, "cairnobsctl dashboards get: missing dashboard id")
|
||||
return 1
|
||||
}
|
||||
return httpGetJSON(apiURL, "/dashboards/"+rest[0], token, stdout, stderr)
|
||||
case "apply":
|
||||
if len(rest) == 0 {
|
||||
fmt.Fprintln(stderr, "sentryctl dashboards apply: missing file path")
|
||||
fmt.Fprintln(stderr, "cairnobsctl dashboards apply: missing file path")
|
||||
return 1
|
||||
}
|
||||
// The import endpoint consumes exactly the shape GET
|
||||
@@ -35,12 +35,12 @@ func cmdDashboards(args []string, stdout, stderr io.Writer) int {
|
||||
return httpPostFileJSON(apiURL, "/dashboards/import", token, rest[0], stdout, stderr)
|
||||
case "permissions":
|
||||
if len(rest) == 0 {
|
||||
fmt.Fprintln(stderr, "sentryctl dashboards permissions: expected a subcommand (list, grant, revoke)")
|
||||
fmt.Fprintln(stderr, "cairnobsctl dashboards permissions: expected a subcommand (list, grant, revoke)")
|
||||
return 1
|
||||
}
|
||||
return cmdDashboardsPermissions(rest, apiURL, token, stdout, stderr)
|
||||
default:
|
||||
fmt.Fprintf(stderr, "sentryctl dashboards: unknown subcommand %q (want list, get, apply, permissions)\n", args[0])
|
||||
fmt.Fprintf(stderr, "cairnobsctl dashboards: unknown subcommand %q (want list, get, apply, permissions)\n", args[0])
|
||||
return 1
|
||||
}
|
||||
}
|
||||
@@ -49,8 +49,8 @@ func cmdDashboards(args []string, stdout, stderr io.Writer) int {
|
||||
// surface -- PUT/DELETE /dashboards/{id}/permissions/{userId} existed
|
||||
// with no caller but Go tests and curl until now (see
|
||||
// /docs/phase-4-runbook.md's "Known gaps"). Kept as dashboards'
|
||||
// own sub-subcommand rather than a flat sentryctl command (like
|
||||
// "sentryctl dashboard-permissions grant ...") since a grant only ever
|
||||
// own sub-subcommand rather than a flat cairnobsctl command (like
|
||||
// "cairnobsctl dashboard-permissions grant ...") since a grant only ever
|
||||
// makes sense in the context of one specific dashboard -- args[0]
|
||||
// selects list/grant/revoke.
|
||||
func cmdDashboardsPermissions(args []string, apiURL, token string, stdout, stderr io.Writer) int {
|
||||
@@ -58,13 +58,13 @@ func cmdDashboardsPermissions(args []string, apiURL, token string, stdout, stder
|
||||
switch sub {
|
||||
case "list":
|
||||
if len(rest) == 0 {
|
||||
fmt.Fprintln(stderr, "sentryctl dashboards permissions list: missing dashboard id")
|
||||
fmt.Fprintln(stderr, "cairnobsctl dashboards permissions list: missing dashboard id")
|
||||
return 1
|
||||
}
|
||||
return httpGetJSON(apiURL, "/dashboards/"+rest[0]+"/permissions", token, stdout, stderr)
|
||||
case "grant":
|
||||
if len(rest) < 3 {
|
||||
fmt.Fprintln(stderr, "sentryctl dashboards permissions grant: usage: grant <dashboard-id> <user-id> <viewer|editor>")
|
||||
fmt.Fprintln(stderr, "cairnobsctl dashboards permissions grant: usage: grant <dashboard-id> <user-id> <viewer|editor>")
|
||||
return 1
|
||||
}
|
||||
dashboardID, userID, role := rest[0], rest[1], rest[2]
|
||||
@@ -73,7 +73,7 @@ func cmdDashboardsPermissions(args []string, apiURL, token string, stdout, stder
|
||||
// only ever raises someone as high as Editor; the server
|
||||
// rejects anything else too, this just fails faster/locally.
|
||||
if role != "viewer" && role != "editor" {
|
||||
fmt.Fprintf(stderr, "sentryctl dashboards permissions grant: role must be \"viewer\" or \"editor\", got %q\n", role)
|
||||
fmt.Fprintf(stderr, "cairnobsctl dashboards permissions grant: role must be \"viewer\" or \"editor\", got %q\n", role)
|
||||
return 1
|
||||
}
|
||||
body := fmt.Sprintf(`{"role":%q}`, role)
|
||||
@@ -81,14 +81,14 @@ func cmdDashboardsPermissions(args []string, apiURL, token string, stdout, stder
|
||||
return httpMutateNoBody(http.MethodPut, apiURL, path, token, body, "granted", stdout, stderr)
|
||||
case "revoke":
|
||||
if len(rest) < 2 {
|
||||
fmt.Fprintln(stderr, "sentryctl dashboards permissions revoke: usage: revoke <dashboard-id> <user-id>")
|
||||
fmt.Fprintln(stderr, "cairnobsctl dashboards permissions revoke: usage: revoke <dashboard-id> <user-id>")
|
||||
return 1
|
||||
}
|
||||
dashboardID, userID := rest[0], rest[1]
|
||||
path := "/dashboards/" + dashboardID + "/permissions/" + userID
|
||||
return httpMutateNoBody(http.MethodDelete, apiURL, path, token, "", "revoked", stdout, stderr)
|
||||
default:
|
||||
fmt.Fprintf(stderr, "sentryctl dashboards permissions: unknown subcommand %q (want list, grant, revoke)\n", sub)
|
||||
fmt.Fprintf(stderr, "cairnobsctl dashboards permissions: unknown subcommand %q (want list, grant, revoke)\n", sub)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
// parsePingArgs resolves the api base URL for ping: --api flag wins, then
|
||||
// $SENTRYCTL_API_URL, then the hardcoded default. Kept pure (env passed in
|
||||
// $CAIRNOBSCTL_API_URL, then the hardcoded default. Kept pure (env passed in
|
||||
// as a function) and separate from the HTTP call so it's unit-testable
|
||||
// without a real environment or server.
|
||||
func parsePingArgs(args []string, env func(string) string) string {
|
||||
@@ -28,7 +28,7 @@ type queryArgs struct {
|
||||
|
||||
// parseQueryArgs is pure (env passed in, no I/O), same testability
|
||||
// reasoning as parsePingArgs. Non-flag arguments are joined with spaces
|
||||
// to form the query, so `sentryctl query service=api status=500` (no
|
||||
// to form the query, so `cairnobsctl query service=api status=500` (no
|
||||
// quotes, no shell-special characters) works without requiring users to
|
||||
// quote every query -- though anything using "|" still needs shell
|
||||
// quoting regardless, since that's a real shell pipe character otherwise.
|
||||
@@ -97,7 +97,7 @@ func cmdQuery(args []string, stdout, stderr io.Writer) int {
|
||||
}
|
||||
|
||||
if strings.TrimSpace(qa.query) == "" {
|
||||
fmt.Fprintln(stderr, "sentryctl query: missing query string")
|
||||
fmt.Fprintln(stderr, "cairnobsctl query: missing query string")
|
||||
return 1
|
||||
}
|
||||
return runAndPrintQuery(qa.apiURL, qa.query, qa.language, qa.jsonOut, stdout, stderr)
|
||||
@@ -3,7 +3,7 @@
|
||||
// runbook, and api/localauth's package doc comment for the full
|
||||
// feature). Same list/create/delete shape as agents/dashboards, plus a
|
||||
// "login" subcommand: unlike every other resource this CLI manages,
|
||||
// there's no way to get a first SENTRYCTL_TOKEN without one.
|
||||
// there's no way to get a first CAIRNOBSCTL_TOKEN without one.
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
|
||||
func cmdUsers(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintln(stderr, "sentryctl users: expected a subcommand (login, list, create, delete, reset-password)")
|
||||
fmt.Fprintln(stderr, "cairnobsctl users: expected a subcommand (login, list, create, delete, reset-password)")
|
||||
return 1
|
||||
}
|
||||
apiURL, rest := extractAPIFlag(args[1:], os.Getenv)
|
||||
@@ -27,7 +27,7 @@ func cmdUsers(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
||||
switch args[0] {
|
||||
case "login":
|
||||
if len(rest) == 0 {
|
||||
fmt.Fprintln(stderr, "sentryctl users login: missing username")
|
||||
fmt.Fprintln(stderr, "cairnobsctl users login: missing username")
|
||||
return 1
|
||||
}
|
||||
return cmdUsersLogin(rest[0], rest[1:], apiURL, stdin, stdout, stderr)
|
||||
@@ -35,24 +35,24 @@ func cmdUsers(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
||||
return httpGetJSON(apiURL, "/auth/users", token, stdout, stderr)
|
||||
case "create":
|
||||
if len(rest) == 0 {
|
||||
fmt.Fprintln(stderr, "sentryctl users create: missing username")
|
||||
fmt.Fprintln(stderr, "cairnobsctl users create: missing username")
|
||||
return 1
|
||||
}
|
||||
return cmdUsersCreate(rest[0], rest[1:], apiURL, token, stdin, stdout, stderr)
|
||||
case "delete":
|
||||
if len(rest) == 0 {
|
||||
fmt.Fprintln(stderr, "sentryctl users delete: missing user id")
|
||||
fmt.Fprintln(stderr, "cairnobsctl users delete: missing user id")
|
||||
return 1
|
||||
}
|
||||
return httpMutateNoBody(http.MethodDelete, apiURL, "/auth/users/"+rest[0], token, "", "user deleted", stdout, stderr)
|
||||
case "reset-password":
|
||||
if len(rest) == 0 {
|
||||
fmt.Fprintln(stderr, "sentryctl users reset-password: missing user id")
|
||||
fmt.Fprintln(stderr, "cairnobsctl users reset-password: missing user id")
|
||||
return 1
|
||||
}
|
||||
return cmdUsersResetPassword(rest[0], rest[1:], apiURL, token, stdin, stdout, stderr)
|
||||
default:
|
||||
fmt.Fprintf(stderr, "sentryctl users: unknown subcommand %q (want login, list, create, delete, reset-password)\n", args[0])
|
||||
fmt.Fprintf(stderr, "cairnobsctl users: unknown subcommand %q (want login, list, create, delete, reset-password)\n", args[0])
|
||||
return 1
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,7 @@ func extractPasswordStdinFlag(args []string) (useStdin bool, rest []string) {
|
||||
// readPasswordFromStdin reads a single line from stdin. Not masked
|
||||
// (this codebase has no terminal/raw-mode dependency to draw on -- see
|
||||
// resolveToken's doc comment for the same tradeoff already accepted for
|
||||
// SENTRYCTL_TOKEN); pipe the value in (`echo "$PW" | sentryctl users
|
||||
// CAIRNOBSCTL_TOKEN); pipe the value in (`echo "$PW" | cairnobsctl users
|
||||
// login admin`) rather than typing it at an interactive terminal where
|
||||
// that matters.
|
||||
func readPasswordFromStdin(stdin io.Reader) (string, error) {
|
||||
@@ -103,7 +103,7 @@ type loginResponseBody struct {
|
||||
}
|
||||
|
||||
// cmdUsersLogin prints only the raw token to stdout on success (nothing
|
||||
// else) -- deliberately pipeable: `export SENTRYCTL_TOKEN=$(sentryctl
|
||||
// else) -- deliberately pipeable: `export CAIRNOBSCTL_TOKEN=$(cairnobsctl
|
||||
// users login admin)`.
|
||||
func cmdUsersLogin(username string, _ []string, apiURL string, stdin io.Reader, stdout, stderr io.Writer) int {
|
||||
password, err := readPasswordFromStdin(stdin)
|
||||
@@ -39,7 +39,7 @@ func TestCmdUsersLoginPrintsOnlyTheToken(t *testing.T) {
|
||||
t.Fatalf("code = %d, want 0; stderr=%s", code, stderr.String())
|
||||
}
|
||||
if got := strings.TrimSpace(stdout.String()); got != "abc123" {
|
||||
t.Fatalf("stdout = %q, want exactly the raw token (pipeable into SENTRYCTL_TOKEN)", got)
|
||||
t.Fatalf("stdout = %q, want exactly the raw token (pipeable into CAIRNOBSCTL_TOKEN)", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ func TestCmdUsersLoginFailure(t *testing.T) {
|
||||
t.Fatalf("stderr = %q, want it to surface the server's error message", stderr.String())
|
||||
}
|
||||
if stdout.String() != "" {
|
||||
t.Fatalf("stdout = %q, want empty on failure (nothing pipeable into SENTRYCTL_TOKEN)", stdout.String())
|
||||
t.Fatalf("stdout = %q, want empty on failure (nothing pipeable into CAIRNOBSCTL_TOKEN)", stdout.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
var httpClient = &http.Client{Timeout: 30 * time.Second}
|
||||
|
||||
// setAuth attaches SENTRYCTL_TOKEN (see resolveToken) as a Bearer
|
||||
// setAuth attaches CAIRNOBSCTL_TOKEN (see resolveToken) as a Bearer
|
||||
// credential, a no-op when token is empty -- matches every backend's
|
||||
// nil-authorizer no-op default (see api/internal/authz.RequireRole*).
|
||||
func setAuth(req *http.Request, token string) {
|
||||
@@ -1,4 +1,4 @@
|
||||
// Command sentryctl is Sentry's control CLI. Six subcommands now
|
||||
// Command cairnobsctl is Cairn OBS's control CLI. Six subcommands now
|
||||
// (ping, query, dashboards, alerts) clearly justify splitting dispatch
|
||||
// across files -- see cli/README.md's "revisit once there's a real
|
||||
// command tree" note -- while keeping the same hand-rolled switch on
|
||||
@@ -46,34 +46,34 @@ func run(args []string, stdout, stderr io.Writer) int {
|
||||
usage(stdout)
|
||||
return 0
|
||||
default:
|
||||
fmt.Fprintf(stderr, "sentryctl: unknown command %q\n", args[0])
|
||||
fmt.Fprintf(stderr, "cairnobsctl: unknown command %q\n", args[0])
|
||||
usage(stderr)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
func usage(w io.Writer) {
|
||||
fmt.Fprintln(w, `sentryctl: Sentry control CLI
|
||||
fmt.Fprintln(w, `cairnobsctl: Cairn OBS control CLI
|
||||
|
||||
Usage:
|
||||
sentryctl ping [--api <url>]
|
||||
sentryctl query "<query>" [--api <url>] [--language sql|spl] [--json]
|
||||
sentryctl dashboards list|get <id>|apply <file> [--api <url>]
|
||||
sentryctl dashboards permissions list <dashboard-id> [--api <url>]
|
||||
sentryctl dashboards permissions grant <dashboard-id> <user-id> viewer|editor [--api <url>]
|
||||
sentryctl dashboards permissions revoke <dashboard-id> <user-id> [--api <url>]
|
||||
sentryctl alerts list|get <id>|apply <file> [--alerting-api <url>]
|
||||
sentryctl agents list|get <host> [--api <url>]
|
||||
sentryctl agents config get <host>|clear <host> [--api <url>]
|
||||
sentryctl agents config set <host> [--batch-max-size N] [--batch-flush-interval-ms N]
|
||||
cairnobsctl ping [--api <url>]
|
||||
cairnobsctl query "<query>" [--api <url>] [--language sql|spl] [--json]
|
||||
cairnobsctl dashboards list|get <id>|apply <file> [--api <url>]
|
||||
cairnobsctl dashboards permissions list <dashboard-id> [--api <url>]
|
||||
cairnobsctl dashboards permissions grant <dashboard-id> <user-id> viewer|editor [--api <url>]
|
||||
cairnobsctl dashboards permissions revoke <dashboard-id> <user-id> [--api <url>]
|
||||
cairnobsctl alerts list|get <id>|apply <file> [--alerting-api <url>]
|
||||
cairnobsctl agents list|get <host> [--api <url>]
|
||||
cairnobsctl agents config get <host>|clear <host> [--api <url>]
|
||||
cairnobsctl agents config set <host> [--batch-max-size N] [--batch-flush-interval-ms N]
|
||||
[--heartbeat-enabled true|false] [--heartbeat-interval-ms N]
|
||||
[--journald-unit UNIT] [--api <url>]
|
||||
sentryctl agents restart <host> [--yes] [--api <url>]
|
||||
sentryctl users login <username> [--password <pw>] [--api <url>]
|
||||
sentryctl users list [--api <url>]
|
||||
sentryctl users create <username> [--password <pw>] [--role viewer|editor|admin|owner] [--api <url>]
|
||||
sentryctl users delete <id> [--api <url>]
|
||||
sentryctl users reset-password <id> [--password <pw>] [--api <url>]
|
||||
cairnobsctl agents restart <host> [--yes] [--api <url>]
|
||||
cairnobsctl users login <username> [--password <pw>] [--api <url>]
|
||||
cairnobsctl users list [--api <url>]
|
||||
cairnobsctl users create <username> [--password <pw>] [--role viewer|editor|admin|owner] [--api <url>]
|
||||
cairnobsctl users delete <id> [--api <url>]
|
||||
cairnobsctl users reset-password <id> [--password <pw>] [--api <url>]
|
||||
|
||||
Commands:
|
||||
ping Checks that the api service is reachable via GET /healthz.
|
||||
@@ -106,19 +106,19 @@ Commands:
|
||||
api/localauth -- only meaningful on a deployment with
|
||||
LOCAL_AUTH_ENABLED set; a 404 on any of these means it
|
||||
isn't). "login" is the only command that works with no
|
||||
$SENTRYCTL_TOKEN set yet -- it prints just the raw token
|
||||
to stdout: `+"`export SENTRYCTL_TOKEN=$(sentryctl users login admin)`"+`.
|
||||
$CAIRNOBSCTL_TOKEN set yet -- it prints just the raw token
|
||||
to stdout: `+"`export CAIRNOBSCTL_TOKEN=$(cairnobsctl users login admin)`"+`.
|
||||
--password (on any users subcommand) is read from stdin
|
||||
if omitted -- same shell-history/ps caveat as typing a
|
||||
credential in any flag, prefer piping it in.
|
||||
"create"/"list"/"delete"/"reset-password" require an
|
||||
owner-role token (see RegisterRoutes in api/localauth).
|
||||
|
||||
--api defaults to $SENTRYCTL_API_URL, or `+defaultAPIURL+` if unset.
|
||||
--alerting-api defaults to $SENTRYCTL_ALERTING_API_URL, or `+defaultAlertingURL+` if unset.
|
||||
--api defaults to $CAIRNOBSCTL_API_URL, or `+defaultAPIURL+` if unset.
|
||||
--alerting-api defaults to $CAIRNOBSCTL_ALERTING_API_URL, or `+defaultAlertingURL+` if unset.
|
||||
--language overrides auto-detection; omit it for the common case.
|
||||
|
||||
$SENTRYCTL_TOKEN, if set, is sent as "Authorization: Bearer <token>" on
|
||||
$CAIRNOBSCTL_TOKEN, if set, is sent as "Authorization: Bearer <token>" on
|
||||
every request -- required once a deployment configures enterprise-auth
|
||||
(see /docs/phase-4-rbac-design.md). No flag equivalent, deliberately:
|
||||
unlike --api, a credential shouldn't be typed where shell history or
|
||||
@@ -126,25 +126,25 @@ unlike --api, a credential shouldn't be typed where shell history or
|
||||
}
|
||||
|
||||
func resolveAPIURL(env func(string) string) string {
|
||||
if v := env("SENTRYCTL_API_URL"); v != "" {
|
||||
if v := env("CAIRNOBSCTL_API_URL"); v != "" {
|
||||
return v
|
||||
}
|
||||
return defaultAPIURL
|
||||
}
|
||||
|
||||
func resolveAlertingURL(env func(string) string) string {
|
||||
if v := env("SENTRYCTL_ALERTING_API_URL"); v != "" {
|
||||
if v := env("CAIRNOBSCTL_ALERTING_API_URL"); v != "" {
|
||||
return v
|
||||
}
|
||||
return defaultAlertingURL
|
||||
}
|
||||
|
||||
// resolveToken reads the RoleService/human bearer credential sentryctl
|
||||
// resolveToken reads the RoleService/human bearer credential cairnobsctl
|
||||
// presents to api/alerting once enterprise-auth enforcement is turned
|
||||
// on (api/internal/authz.RequireRole*) -- empty by default, matching
|
||||
// every other Phase 0-3 client's nil-authorizer no-op behavior.
|
||||
func resolveToken(env func(string) string) string {
|
||||
return env("SENTRYCTL_TOKEN")
|
||||
return env("CAIRNOBSCTL_TOKEN")
|
||||
}
|
||||
|
||||
type errorResponseBody struct {
|
||||
@@ -18,7 +18,7 @@ func TestParsePingArgsDefault(t *testing.T) {
|
||||
|
||||
func TestParsePingArgsFromEnv(t *testing.T) {
|
||||
env := func(k string) string {
|
||||
if k == "SENTRYCTL_API_URL" {
|
||||
if k == "CAIRNOBSCTL_API_URL" {
|
||||
return "http://env-host:1234"
|
||||
}
|
||||
return ""
|
||||
@@ -30,7 +30,7 @@ func TestParsePingArgsFromEnv(t *testing.T) {
|
||||
|
||||
func TestParsePingArgsFlagOverridesEnv(t *testing.T) {
|
||||
env := func(k string) string {
|
||||
if k == "SENTRYCTL_API_URL" {
|
||||
if k == "CAIRNOBSCTL_API_URL" {
|
||||
return "http://env-host:1234"
|
||||
}
|
||||
return ""
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
module github.com/sentry/sentry/cli
|
||||
module github.com/cairnobs/cairnobs/cli
|
||||
|
||||
go 1.25
|
||||
|
||||
Reference in New Issue
Block a user