Skip to content

v0.5.0

Latest

Choose a tag to compare

@github-actions github-actions released this 17 Aug 19:07
· 3 commits to main since this release
v0.5.0
5d9e0d6

Added

  • Map identity-provider groups to roles from the app. Which SSO group
    grants which role on which projects was a chart value — auth.oidc.mapping
    in values.yaml, one helm upgrade per change — so giving a team access
    meant a deploy. Settings → Access now shows those rules and lets an admin
    add, edit and delete rules of their own beside them. The chart stays the
    declared base: its rules render read-only, and on a name collision the chart
    wins — an authored rule for a group the chart also declares is kept and
    marked as overridden, with the reason on its row, instead of being silently
    ignored or refused outright. A change applies to that group's next sign-in
    or token refresh and reaches every hub replica within about fifteen seconds;
    that bound is stated in the panel, so a stale read on another replica right
    after a save is not mistaken for a failed one. Reset deletes every rule
    authored in the app and returns the install to exactly what the chart
    declares. An install with no identity provider configured never sees the
    panel — there is nothing for it to map.

  • Personal API tokens: your scripts get a key of their own. Until now the
    only credential the hub accepted was a browser session's cookie, so a script,
    a CI job or a future client had nothing to authenticate with. Settings →
    Access now mints named tokens with an optional expiry; sent as
    Authorization: Bearer avurut_…, a token authenticates the request as its
    owner. The secret is shown exactly once and only its SHA-256 is stored, so
    no database read — and no admin — can recover it later; the list shows each
    token's prefix and when it was last used, which is what "is this one still
    needed?" actually takes to answer. A token deliberately carries no
    permissions of its own: it resolves to its owner's live grants at request
    time, so demoting a user demotes their tokens in the same moment and
    disabling the account silences every token it holds. A bad, expired or
    revoked token is a 401, never a quiet fall-through to the anonymous role a
    demo install grants cookieless visitors. Managing tokens requires only being
    signed in — deliberately no role floor, so a user whose grants were all
    revoked can still clean up the credentials they handed out, for the same
    reason they can still log out — and a global admin can widen the list to
    another user's tokens to audit them. Revoking a token you don't own answers
    404, not 403, so the endpoint confirms nothing about other people's keys.

  • The service map now says what is wrong, not just that something is.

    A node used to turn red the moment any error appeared in the window — a
    binary that never distinguished one failed health check from an outage. Its
    ring is now the service's actual status (healthy, degraded, down, idle), read
    from the same dependency-aware rollup the Service Health board uses. The map
    deliberately does not re-derive those thresholds: they are configurable per
    group and live in the hub, so a second copy in the browser would drift and the
    two screens would quietly disagree. A service the rollup does not cover reads
    as unknown — never as healthy.

    Edges carry real latency for the first time: p50 and p95 measured from the
    caller's span, which is what that call path actually cost including
    network and queueing. That is deliberately not the callee's own server-side
    p95, which the node already shows, and the gap between the two is usually the
    point — in the seeded demo a node reads p95 200ms while the edge into it
    reads p95 220ms, so 20ms is being paid somewhere the callee cannot see. It
    costs one extra aggregate on a join the query already ran. Edges derived from
    network flows have no span to measure, so they omit the field rather than
    report a false 0ms.

    Hovering a node fades everything outside its neighbourhood and labels its
    edges with rpm, p95, error rate and TCP RTT where measured. Search, a
    problems-only toggle and a group filter all live in the URL, so a narrowed map
    is a link rather than a screen you describe over a call; zoom, fit and a
    legend round it out. The status and group filters appear only when service
    health is running, since both read its rollup — and on an install without it
    the ring falls back to the previous error-presence signal rather than going
    quietly blank.

    The carbon lens moved from the node border to a halo around it. The border was
    the only one a node had, and the status ring now needs it; as a halo, a node
    shows its health and its gCO2e at once instead of one overwriting the other.
    The Dashboard's compact topology is the same component, so it gained all of
    this without a second implementation to keep in step.

  • One screen for how the estate is doing. Everything the product knew lived
    behind a hypothesis you had to already have: traces if you knew the service,
    nodes if you knew it was capacity, alerts if you knew something had fired.
    Opening the app told you nothing until you had a guess. The Dashboard is now
    the landing route and gives you one — service-group health, live topology
    beside the firing alerts, and Kubernetes capacity, in three bands.

    It is fixed on purpose: no widget model, no layout editor, no persistence.
    Every band reads an API that already existed, so the screen added no hub
    surface at all, and each band follows its own module — bands whose module is
    off simply do not mount, so the screen never shows a panel that would 404.
    With service health off, the summary band falls back to the busiest services
    and those fallback cards carry no status: thresholds and dependency
    propagation belong to that module, and inventing a second set here would put
    two answers to one question on the same screen.

    One honest gap: there is no CPU utilization percentage anywhere on it. Nothing
    in the collection path reports allocatable CPU, so a percentage would need a
    denominator the install does not have — capacity reports cores in use, and
    only memory shows both halves of a real bar.

  • Say which services matter, from the app. Service health groups — a name,
    a criticality tier and the namespaces or services it covers — are now created,
    edited and deleted in Settings → Groups, and apply to the next health read.
    Until now the only way to define one was serviceGroups in values.yaml
    followed by a helm upgrade, which meant that in practice nobody did: the
    Service Health board showed one auto-discovered group per namespace and the
    tier lanes stayed empty. Auto-grouping still works exactly as before, so
    nothing disappears while you organize, and the board now links straight to the
    editor instead of naming a config key.

    Groups declared in the chart keep working and render read-only, because the
    config wins a name collision — an install that manages its groups in Git must
    not have them quietly overridden from a browser, so the conflict is refused at
    write time rather than discovered at the next upgrade. Writes are admin-only
    and go through the same validation the ConfigMap loader applies at boot, so
    the API cannot store a group that would fail the next restart.

    The merge of the two sources happens in exactly one place, shared by the API
    and the alerting evaluator. The evaluator does not go through the API, so
    merging in a handler would have meant a group you created showing as critical
    on the health board while alerting never paged on it — a divergence pinned by
    a test that drives both paths and then fires a real rule
    (design/2026-08-07-service-groups-crud.md).

  • Two new Settings tabs: Storage, and Access.

    Storage answers "where is my telemetry and how much of it is there".
    The ClickHouse address, database and user, read-only — not as a missing
    feature but because ClickHouse is the store, so it cannot hold its own
    connection string; the card says so and gives the --set line instead of a
    form that would be a lie. It is reported even while ClickHouse is
    unreachable, which is when "what address did we fail to reach?" is the first
    question. Then per-signal size, compression, row count, age and retention,
    moved here from Status so each tab answers one question: Status is "is it
    healthy right now", Storage is "what is in it".

    Retention now shows two numbers when they disagree. The days in your values
    are what the install is configured to keep; the TTL on the tables is what
    ClickHouse is enforcing, and changing a retention value does nothing to
    tables that already exist until the migration re-applies it. Until then the
    configured number is a wish, and the column says 30d → 7d rather than
    repeating the wish. A freshly migrated database with retention not yet
    applied reads → none.

    Access shows which role may do what, per area of the product. Every cell
    is derived by the hub from the authorization its routes registered with, not
    written out a second time in the browser: routes register through an index
    that records their guard, so adding an admin-only endpoint puts it in the
    matrix and changing a guard changes the matrix with it. A table that can
    disagree with the middleware is worse than no table, because it gets
    believed. An install running without authentication says so at the top,
    instead of presenting a model nothing is enforcing.

  • Turn signals on and off from the UI, without a redeploy. Settings →
    Collection becomes writable: an admin switches OBI traces, logs,
    infra-metrics, profiling or energy collection on or off, and edits the
    excluded-namespace list, and the sensor picks the change up in seconds. Until
    now every one of those decisions meant editing values.yaml, running helm upgrade, and holding the cluster permissions to do it — so in practice
    collection was whatever it was at install time. The screen also reports the
    effective configuration (chart values with your overlay applied), so what
    it shows is what the sensor is actually doing, and "reset to defaults" puts
    the cluster back to exactly what the chart declares.

    Off by default (collection.runtimeControl.enabled): opting in grants the
    hub a deliberately narrow Role — get/update/patch on its own four named
    sensor ConfigMaps and get/patch on the named sensor DaemonSet, in its own
    namespace, and nothing else. The hub patches its own annotation to roll the
    DaemonSet, leaving Helm's ownership untouched, so a later helm upgrade
    behaves normally. With the flag off, nothing changes and no extra permissions
    are granted.

    Proven end to end against a real cluster: the Helm smoke gate now writes an
    overlay through the API, asserts it reaches the sensor ConfigMaps and rolls
    the DaemonSet, then resets and asserts the cluster reconciles back.

  • Find a pod on the Nodes screen. Both tables now sort by any column, and
    both filter — nodes by name, pods by name, namespace or workload, with a
    namespace picker that appears once there is more than one namespace to choose
    between. On a real cluster the pods table is a hundred-plus unordered rows,
    and until now the only way through it was the browser's find-in-page.
    Filters live in the URL, so a narrowed view is a link you can send someone,
    and they apply as you type — the rows are already in the browser, so nothing
    waits on a query. When a filter is active the counts read "N of M", so a
    narrowed table can't be misread as a shrinking cluster, and a filter that
    matches nothing says so instead of showing the "install the sensor" empty
    state, which would send you off to debug a perfectly healthy install.

Fixed

  • go test -race ./... ran out of time before it could finish the hub's API
    suite.
    bcrypt cost 12 is a deliberate login-path choice, but the race
    detector makes a hash-and-compare pair cost ~5.5s, and every handler test
    that bootstraps an admin, logs in, or creates a user paid it. internal/api
    spent 503s that way and crossed go test's 10-minute per-package timeout on
    CI — surfacing as a panic in whichever test happened to be running when the
    clock ran out, which is why it read as a hang rather than as accumulated
    cost. The cost now drops to bcrypt.MinCost inside a go test binary and
    nowhere else: the switch is testing.Testing(), which is false in every
    production build, so no flag, environment variable or chart value can reach
    the cheap cost. The dummy hash burned on the unknown-user path tracks the
    same cost — bcrypt reads the cost from the hash, not from the caller, so
    leaving it pinned at 12 would have kept that path slow and hidden half the
    problem. internal/api now runs in 4.7s and internal/auth in 3.3s; the
    production cost of 12 is pinned by a test, as is the dummy's agreement with
    it.

  • The shared demo account was offered a password form it could never
    submit.
    Settings → Account decided whether to render the change-password
    form from the sign-in origin alone — and the demo viewer is a perfectly
    ordinary local account, because EnsureDemoUser creates it as one. So a
    visitor to a demo install could open the tab, type a current and a new
    password, submit, and only then be told the attempt was never possible. The
    hub was right to refuse it (that row is re-created and re-keyed from the
    install's configuration on every boot, so a "successful" change would
    silently revert, and the credential is shared with every other visitor); the
    UI simply wasn't told. /api/v1/auth/me now carries a passwordChange field
    stating whether self-service rotation applies and, when it doesn't, why —
    self, idp (the identity provider owns the credential) or shared (the
    demo account). It reproduces the hub's own refusals in the same order, and
    the Account tab renders the explanation instead of the form. A value this
    build doesn't recognise renders the explanation too: offering a form the
    server will reject is the failure being fixed, so the fallback is never the
    form.

Security

  • The green endpoints served any project's energy and carbon figures to an
    unauthenticated caller.
    GET /api/v1/green/summary, /green/budgets and
    /green/report were registered with the bare handler wrapper instead of the
    session middleware every other signal route uses. Nothing then put an identity
    on the request — and the per-project scope check treats "no identity" as
    "authentication is switched off", the branch that exists so an
    auth.enabled=false install keeps working. So on an install with
    authentication on, those three routes answered 200 with no session at all,
    for any tenant named in the request header: per-service energy in watt-hours,
    carbon in gCO2e, monthly budget usage, and the CSRD-ready report export. They are
    read-only, so nothing could be changed through them, but the data itself is a
    fair map of what an estate runs and how hard. All three now require the viewer
    role and honour project grants, like every other signal. A test enumerates the
    project-data routes and asserts each answers 401 without a session — the gap
    survived precisely because nothing asserted over the whole set.