Skip to content
Merged
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
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ One binary, no config to write, no browser.

## What it does

- Lists jobs, allocations, tasks, task groups, deployments, evaluations,
services, namespaces, variables, clients, node pools, servers, volumes,
CSI plugins and the ACL objects.
- Lists jobs, allocations, the tasks of an allocation or of the whole
namespace, task groups, deployments, evaluations, services, namespaces,
variables, clients, node pools, servers, volumes, CSI plugins and the ACL
objects.
- Watches the deploy of a job with `w`: the newest evaluation, the deployment
and its allocations, until it ends.
- Updates the screen when the cluster changes, using the Nomad event stream,
Expand Down Expand Up @@ -53,7 +54,8 @@ One binary, no config to write, no browser.
with its meta and payload.
- Starts, stops, scales and restarts jobs and allocations, restarts every
task of an allocation with its prestart and sidecar tasks, restarts and
signals single tasks, and opens a shell in a task.
signals single tasks, opens a shell in a task, and runs a command in a task
and keeps what it printed.
- Drains clients, shows their CPU and memory over time, marks the ones that
are full, shows when the identity of a client expires and renews it.
Purges clients that are down, stopped jobs, and collects the garbage of a
Expand Down Expand Up @@ -129,8 +131,8 @@ quit.
keys of every screen.
- [Jobs](docs/jobs.md): jobs, task groups and their scaling, editing and
plans, versions, evaluations and deployments.
- [Allocations and tasks](docs/allocations.md): allocations, checks, task
actions, logs and files.
- [Allocations and tasks](docs/allocations.md): allocations, every task of
the namespace with `:tasks`, checks, task actions, logs and files.
- [Clients, servers and other resources](docs/clients.md): clients,
servers, the scheduler, namespaces, services, variables and node pools.
- [Storage](docs/storage.md): CSI volumes, dynamic host volumes and CSI
Expand Down
35 changes: 33 additions & 2 deletions docs/allocations.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Allocations and tasks

This page covers allocations, the tasks of an allocation, checks, logs and
files. For the keys of each screen, see [Keys and commands](keys.md).
This page covers allocations, the tasks of an allocation, every task of the
namespace with `:tasks`, checks, logs and files. For the keys of each screen,
see [Keys and commands](keys.md).

## Allocation list

Expand Down Expand Up @@ -59,13 +60,36 @@ For each running task, the list shows CPU and memory use as a share of what
the task asked for, updated every 5 seconds. Memory is read the same way as
in the allocation list.

The list also shows Started (how long ago the task last started), Finished
(how long ago it ended, for a task that has ended) and Uptime (how long it has
run since it started, or how long it ran once it ended).

From here:

- `c` opens the client the allocation runs on;
- `p` opens the previous allocation, `n` the next one;
- `f` opens the follow-up evaluation;
- `o` opens the events of the allocation.

## Tasks of the namespace

`:tasks` lists every task of every allocation of the namespace on one screen,
to find the one that is dead, pending or restarting without opening each job
and each allocation. The columns are Job, Namespace, Group, Task, Alloc, Node,
State, Restarts, Started, Finished and Uptime. The rows have the colors of the
tasks screen: failed tasks in red, pending ones in orange, dead ones grey. A
capital letter sorts by a column: `R` by restarts, `U` by uptime.

The keys of a task work on the row: `enter` and `ctrl-e` for the logs, `e` for
the events, `o` for the events of its allocation, `b` for the files, `s` for a
shell, `r` to restart, `x` to send a signal and `ctrl-x` to run a command.
They act on the task in its own allocation and namespace, also when the
session looks at every namespace.

The list is read from the allocation list in one request, so there is no CPU
and memory column: they would cost a request for each task. It holds the
tasks of allocations that ended until the cluster forgets them.

## Checks

The panel lists the checks of the allocation while it is running:
Expand All @@ -90,6 +114,13 @@ These keys work on the task under the cursor while it is running:
- `s` opens a shell in the task. urga starts `bash` if the task has it, and
`sh` if not. The shell opens on a clean screen. What the terminal showed
before, an earlier shell included, is still in its scrollback.
- `ctrl-x` asks for a command and runs it with `/bin/sh -c`, without a
terminal and with nothing on its stdin. It opens what the command printed to
stdout and stderr, in the order it printed it, with the exit code in the
title. `/`, `f`, `c`, `ctrl-s` and `w` work on it as on a log. Of an output
larger than 1 MiB it keeps the last 1 MiB, and the title says so. urga waits
10 seconds for the command and then shows an error. A task without `/bin/sh`
cannot run it.

urga only sends a signal name that Nomad knows. When a Nomad client gets a
name it does not know, it sends `SIGINT` instead, which stops most tasks. So
Expand Down
10 changes: 5 additions & 5 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ urga --address https://nomad.example.com:4646 --region eu --namespace production
With `--readonly`, urga does not change anything in the cluster. It hides
every key that would: start and stop a job, revert, edit, scale, restart or
stop an allocation, restart or signal a task, drain a client, change its
eligibility, promote or fail a deployment, and open a shell. If you press one
of these keys, the status line says that it is off. The header shows
`read-only` next to the address.
eligibility, promote or fail a deployment, open a shell and run a command in
a task. If you press one of these keys, the status line says that it is off.
The header shows `read-only` next to the address.

The shell is off in read-only mode because a shell in a task can change
anything the task can.
The shell and the command are off in read-only mode because a command in a
task can change anything the task can.

## Token

Expand Down
2 changes: 2 additions & 0 deletions docs/jobs.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ From the list:
- `t` opens its task groups;
- `d` shows the job as the cluster describes it;
- `h` shows the job file it was submitted with;
- `m` shows its meta, a key and its value on each row; `c` copies the value
under the cursor. A job without meta says so;
- `v` opens its versions;
- `o` shows the events of the job.

Expand Down
16 changes: 11 additions & 5 deletions docs/keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Press `:` to open the command line, type a command and press `enter`.
| --- | --- | --- |
| `jobs` | `job`, `jb` | Jobs |
| `allocations` | `allocs`, `alloc` | Allocations of the namespace |
| `tasks` | `task` | Every task of the namespace. See [Tasks of the namespace](allocations.md#tasks-of-the-namespace). |
| `deployments` | `dp` | Deployments |
| `evaluations` | `evals`, `eval`, `ev` | Evaluations |
| `events` | `event` | What happens in the cluster, event by event. See [Events](#events). |
Expand Down Expand Up @@ -167,10 +168,11 @@ Shift and a letter order the list by a column; the same key again turns the
order round, and the title of the column shows which way with an arrow.
Every column has a letter of its own. The common ones keep the letters you
know from other tools: `N` for a name, `A` for an age, `S` for a status or a
state, `P` for a namespace. Any other column takes the first letter of its
title that is free, so on `:events` `T` is the topic and `Y` the type. The
header lists the letters of the screen that is open, next to the namespaces,
and so does `?`.
state, `P` for a namespace, `R` for restarts. Any other column takes the
first letter of its title that is free, so on `:events` `T` is the topic and
`Y` the type. `G` is never a column's letter, because it goes to the bottom.
The header lists the letters of the screen that is open, next to the
namespaces, and so does `?`.

## Filter

Expand Down Expand Up @@ -219,6 +221,7 @@ start, stop, restart or drain then applies to every marked row.
| `t` | Task groups | |
| `d` | Describe | |
| `h` | Job file the job was submitted with | |
| `m` | Meta of the job; `c` copies a value | |
| `v` | Versions | |
| `l` | Logs of a task in every allocation of the job. Not shown for a periodic or parameterized job. | |
| `w` | Watch the deploy of the job at the version it runs. Not shown for a periodic or parameterized job. | |
Expand Down Expand Up @@ -303,6 +306,7 @@ On what was done to the count of a group, `d` describes its policy.
| `s` | Shell in the task | yes |
| `r` | Restart the task. Shown when it is running. | yes |
| `x` | Send a signal to the task. Shown when it is running. | yes |
| `ctrl-x` | Run a command in the task and show what it printed. Shown when it is running. | yes |
| `c` | Client the allocation runs on | |
| `p` | Allocation this one replaced | |
| `n` | Allocation that replaced this one | |
Expand All @@ -311,6 +315,8 @@ On what was done to the count of a group, `d` describes its policy.
`p`, `n` and `f` are shown only when there is such an allocation or
evaluation.

The list of `:tasks` has these keys too, except `c`, `p`, `n` and `f`.

### Logs

| Key | What it does |
Expand Down Expand Up @@ -544,7 +550,7 @@ On every list of ACL objects:

### Text screens

Descriptions, job files and diffs:
Descriptions, job files, diffs and the output of a command run with `ctrl-x`:

| Key | What it does |
| --- | --- |
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/BurntSushi/toml v1.6.0
github.com/charmbracelet/x/ansi v0.11.8
github.com/google/licenseclassifier/v2 v2.0.0
github.com/gorilla/websocket v1.5.3
github.com/hashicorp/hcl/v2 v2.25.0
github.com/hashicorp/nomad/api v0.0.0-20260922135024-0afeddadd9b4
github.com/ingvarch/pulse v0.4.0
Expand All @@ -35,7 +36,6 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/cronexpr v1.1.3 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
Expand Down
10 changes: 10 additions & 0 deletions internal/nomad/describe.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ func (c *Client) DescribeJob(ctx context.Context, namespace, jobID string) (stri
return asJSON(job)
}

// JobMeta is the meta of the job block, which is nil when the job has none.
func (c *Client) JobMeta(ctx context.Context, namespace, jobID string) (map[string]string, error) {
job, _, err := c.api.Jobs().Info(jobID, c.query(ctx, namespace))
if err != nil {
return nil, err
}

return job.Meta, nil
}

// DescribeAllocation is what the cluster knows about an allocation.
func (c *Client) DescribeAllocation(ctx context.Context, namespace, allocID string) (string, error) {
alloc, _, err := c.api.Allocations().Info(allocID, c.query(ctx, namespace))
Expand Down
23 changes: 23 additions & 0 deletions internal/nomad/describe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,29 @@ func TestDescribeJob(t *testing.T) {
r.Contains(out, "\n")
}

func TestJobMeta(t *testing.T) {
r := require.New(t)

client, asked := recorder(t, `{"ID": "web", "Meta": {"owner": "team-web", "version": "3"}}`)

meta, err := client.JobMeta(context.Background(), "production", "web")
r.NoError(err)

r.Equal("/v1/job/web", asked.URL.Path)
r.Equal("production", asked.URL.Query().Get("namespace"))
r.Equal(map[string]string{"owner": "team-web", "version": "3"}, meta)
}

func TestJobMeta_AJobWithoutMetaGivesNone(t *testing.T) {
r := require.New(t)

client, _ := recorder(t, `{"ID": "web"}`)

meta, err := client.JobMeta(context.Background(), "production", "web")
r.NoError(err)
r.Empty(meta)
}

func TestDescribeAllocation(t *testing.T) {
r := require.New(t)

Expand Down
108 changes: 102 additions & 6 deletions internal/nomad/shell.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package nomad

import (
"bytes"
"context"
"io"
"strings"
"unicode/utf8"

"github.com/hashicorp/nomad/api"
)
Expand All @@ -24,11 +27,6 @@ func (c *Client) Exec(
stdout, stderr io.Writer,
sizes <-chan TerminalSize,
) (int, error) {
alloc, _, err := c.api.Allocations().Info(allocID, c.query(ctx, namespace))
if err != nil {
return 0, err
}

// The api takes its own size type, so each size is converted and passed
// on as it arrives.
apiSizes := make(chan api.TerminalSize)
Expand All @@ -45,5 +43,103 @@ func (c *Client) Exec(
}
}()

return c.api.Allocations().Exec(ctx, alloc, task, true, command, stdin, stdout, stderr, apiSizes, c.query(ctx, namespace))
return c.exec(ctx, namespace, allocID, task, true, command, stdin, stdout, stderr, apiSizes)
}

// CommandOutput is what a command printed to stdout and stderr, in the order
// the task printed it, and the exit code.
type CommandOutput struct {
Text string
ExitCode int

// Size is how many bytes the command printed, and From how many of them
// were dropped from the start to keep only its end.
Size int64
From int64
}

// outputTail is how much of what a command printed is kept: its last MiB.
// A command such as yes prints hundreds of MiB in the time urga waits for it.
const outputTail = 1 << 20

// RunCommand runs a command inside a task with no terminal and nothing on
// its stdin, and returns what it printed: all of it, or the whole lines of
// its last MiB.
func (c *Client) RunCommand(ctx context.Context, namespace, allocID, task string, command []string) (CommandOutput, error) {
printed := tail{limit: outputTail}

code, err := c.exec(ctx, namespace, allocID, task, false, command, strings.NewReader(""), &printed, &printed, nil)
if err != nil {
// The api may still be writing to the buffer.
return CommandOutput{}, err
}

text := printed.text()

return CommandOutput{Text: string(text), ExitCode: code, Size: printed.size, From: printed.size - int64(len(text))}, nil
}

// tail keeps the last limit bytes written to it, and the byte before them,
// and counts all of them.
type tail struct {
limit int
kept []byte
size int64
}

func (t *tail) Write(p []byte) (int, error) {
// The byte before the last limit bytes says whether a line starts
// right where they start.
keep := t.limit + 1

t.size += int64(len(p))
t.kept = append(t.kept, p[max(len(p)-keep, 0):]...)

// The start is dropped only once twice the limit is held, so a byte is
// copied a few times at most, not once per write.
if len(t.kept) > 2*t.limit {
t.kept = append(t.kept[:0], t.kept[len(t.kept)-keep:]...)
}

return len(p), nil
}

// text is all that was written, or when that was more than the limit, the
// lines that start within its last limit bytes. When no line starts there,
// it is those bytes from the first character that starts within them.
func (t *tail) text() []byte {
if t.size <= int64(t.limit) {
return t.kept
}

kept := t.kept[len(t.kept)-t.limit-1:]
if at := bytes.IndexByte(kept[:len(kept)-1], '\n'); at >= 0 {
return kept[at+1:]
}

// A character cut by the limit loses its first bytes, so the rest of it
// is dropped too.
start := 1
for start < min(len(kept), utf8.UTFMax) && !utf8.RuneStart(kept[start]) {
start++
}

return kept[start:]
}

func (c *Client) exec(
ctx context.Context,
namespace, allocID, task string,
tty bool,
command []string,
stdin io.Reader,
stdout, stderr io.Writer,
sizes <-chan api.TerminalSize,
) (int, error) {
alloc, _, err := c.api.Allocations().Info(allocID, c.query(ctx, namespace))
if err != nil {
return 0, err
}

return c.api.Allocations().Exec(ctx, alloc, task, tty, command, stdin, stdout, stderr, sizes, c.query(ctx, namespace))
}
Loading
Loading