tailkit

package module
v0.1.8 Latest Latest
Warning

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

Go to latest
Published: Mar 21, 2026 License: MIT Imports: 22 Imported by: 0

README

tailkit-README.md

tailkit

Go library for building Tailscale-native tools. tailkit has two distinct concerns:

  1. tsnet utilities — useful for any tailnet tool regardless of tailkitd
  2. tailkitd client SDK — typed HTTP client for every tailkitd endpoint

Tools built with tailkit get consistent auth, peer discovery, and access to node-level integrations (Docker, systemd, metrics, files, vars, exec) across every node running tailkitd.


Install

go get github.com/wf-pro-dev/tailkit

tsnet utilities

These are useful for any tsnet-based tool, regardless of whether tailkitd is installed on the target nodes.

NewServer

Handles the three things every tool gets wrong independently: auth key resolution, TLS setup via lc.GetCertificate, and graceful shutdown on OS signal.

srv, err := tailkit.NewServer(tailkit.ServerConfig{
    Hostname: "devbox",
    AuthKey:  os.Getenv("TS_AUTHKEY"),
})
defer srv.Close()
AuthMiddleware

Runs lc.WhoIs on every inbound request. Injects caller hostname, Tailscale IP, login name, and application capabilities into the request context. Replaces the callerHost + manual WhoIs boilerplate every tool currently reimplements.

var h http.Handler = yourMux
h = tailkit.AuthMiddleware(srv)(h)

Retrieve caller identity inside a handler:

id, ok := tailkit.CallerFromContext(r.Context())
// id.Hostname, id.TailscaleIP, id.UserLogin, id.Caps

Tool registration

Tools call tailkit.Install() once at install time and on every upgrade. This writes a JSON file to /etc/tailkitd/tools/{name}.json that tailkitd reads to populate its tool registry and exec command list.

err := tailkit.Install(ctx, tailkit.Tool{
    Name:      "devbox",
    Version:   build.Version,
    TsnetHost: "devbox",            // the tsnet hostname this tool registers
    Commands: []tailkit.Command{
        {
            Name:        "reload-nginx",
            Description: "Reloads nginx after a config push",
            ACLCap:      "tailscale.com/cap/devbox",
            ExecParts:   []string{"/usr/bin/systemctl", "reload", "nginx"},
            Timeout:     30 * time.Second,
        },
        {
            Name:        "restart-container",
            Description: "Restarts a named Docker container",
            ACLCap:      "tailscale.com/cap/devbox",
            ExecParts:   []string{"/usr/bin/docker", "restart", "{{.container}}"},
            Timeout:     60 * time.Second,
            Args: []tailkit.Arg{
                {
                    Name:     "container",
                    Type:     "string",
                    Required: true,
                    Pattern:  tailkit.PatternIdentifier,
                },
            },
        },
    },
})

Uninstall removes the registration file:

err := tailkit.Uninstall("devbox")
Arg pattern constants
tailkit.PatternIdentifier  // ^[a-zA-Z0-9_-]+$       container names, service names
tailkit.PatternPath        // ^(/[a-zA-Z0-9_./-]+)+$  absolute unix paths
tailkit.PatternSemver      // ^v?[0-9]+\.[0-9]+\.[0-9]+$
tailkit.PatternIP          // ^(\d{1,3}\.){3}\d{1,3}$
tailkit.PatternPort        // ^([1-9][0-9]{0,4})$
tailkit.PatternFilename    // ^[a-zA-Z0-9_.,-]+$

Node client

All client methods are accessed via tailkit.Node(srv, hostname). The tsnet.Server is the caller's own server — tailkit uses it to open a direct tsnet connection to tailkitd.<hostname>.ts.net.

Tools
tools, err := tailkit.Node(srv, "vps-1").Tools(ctx)
ok, err    := tailkit.Node(srv, "vps-1").HasTool(ctx, "devbox", "1.2.0")
Exec
// fire and forget — returns immediately with job_id
job, err := tailkit.Node(srv, "vps-1").Exec(ctx, "devbox", "reload-nginx", nil)

// fire and poll until complete — blocks until done or context cancelled
result, err := tailkit.Node(srv, "vps-1").ExecWait(ctx, "devbox", "restart-container",
    map[string]string{"container": "my-app"},
)

// poll an existing job
result, err := tailkit.Node(srv, "vps-1").ExecJob(ctx, job.JobID)
Files
// read file content as string (Accept: application/json)
content, err := tailkit.Node(srv, "vps-1").Files().Read(ctx, "/opt/myapp/compose.yml")

// download to a local path (Accept: application/octet-stream)
err = tailkit.Node(srv, "vps-1").Files().Download(ctx,
    "/opt/myapp/compose.yml",
    "/local/dest/compose.yml",
)

// list directory
entries, err := tailkit.Node(srv, "vps-1").Files().List(ctx, "/opt/myapp/")

// send a local file to the node
result, err := tailkit.Node(srv, "vps-1").Send(ctx, tailkit.SendRequest{
    LocalPath: "/home/user/nginx/api.conf",
    DestPath:  "/etc/nginx/conf.d/api.conf",
})

// send a local directory to the node
result, err := tailkit.Node(srv, "vps-1").SendDir(ctx, tailkit.SendDirRequest{
    LocalDir: "/home/user/nginx/",
    DestPath: "/etc/nginx/conf.d/",
})

// wait for the post-receive hook to complete
if result.JobID != "" {
    hookResult, err := tailkit.Node(srv, "vps-1").ExecJob(ctx, result.JobID)
}
Vars
// list all vars in a scope
vars, err := tailkit.Node(srv, "vps-1").Vars("myapp", "prod").List(ctx)

// get single var
val, err := tailkit.Node(srv, "vps-1").Vars("myapp", "prod").Get(ctx, "DATABASE_URL")

// set a var
err = tailkit.Node(srv, "vps-1").Vars("myapp", "prod").Set(ctx, "LOG_LEVEL", "debug")

// delete a var
err = tailkit.Node(srv, "vps-1").Vars("myapp", "prod").Delete(ctx, "LOG_LEVEL")

// render as KEY=VALUE text
envText, err := tailkit.Node(srv, "vps-1").Vars("myapp", "prod").Env(ctx)
ExecWith

Fetch vars from a node and inject them into a local process without touching disk. Secrets exist only in the process environment and disappear when the process exits.

vars, err := tailkit.Node(srv, "vps-1").Vars("myapp", "prod").List(ctx)
err        = tailkit.ExecWith(ctx, vars, []string{"/usr/bin/node", "server.js"})
Docker
// containers
containers, err := tailkit.Node(srv, "vps-1").Docker().Containers(ctx)
detail, err     := tailkit.Node(srv, "vps-1").Docker().Container(ctx, "my-app")
logs, err       := tailkit.Node(srv, "vps-1").Docker().Logs(ctx, "my-app", 100)
stats, err      := tailkit.Node(srv, "vps-1").Docker().Stats(ctx, "my-app")
err              = tailkit.Node(srv, "vps-1").Docker().Start(ctx, "my-app")
err              = tailkit.Node(srv, "vps-1").Docker().Stop(ctx, "my-app")
err              = tailkit.Node(srv, "vps-1").Docker().Restart(ctx, "my-app")
err              = tailkit.Node(srv, "vps-1").Docker().Remove(ctx, "my-app")

// images
images, err := tailkit.Node(srv, "vps-1").Docker().Images(ctx)
job, err    := tailkit.Node(srv, "vps-1").Docker().Pull(ctx, "nginx:latest")

// compose
projects, err := tailkit.Node(srv, "vps-1").Docker().Compose().Projects(ctx)
project, err  := tailkit.Node(srv, "vps-1").Docker().Compose().Project(ctx, "myapp")
job, err      := tailkit.Node(srv, "vps-1").Docker().Compose().Up(ctx, "myapp", "/opt/myapp/compose.yml")
job, err       = tailkit.Node(srv, "vps-1").Docker().Compose().Down(ctx, "myapp")
job, err       = tailkit.Node(srv, "vps-1").Docker().Compose().Pull(ctx, "myapp")
job, err       = tailkit.Node(srv, "vps-1").Docker().Compose().Restart(ctx, "myapp")
job, err       = tailkit.Node(srv, "vps-1").Docker().Compose().Build(ctx, "myapp")

// swarm (read only in v1)
nodes, err    := tailkit.Node(srv, "vps-1").Docker().Swarm().Nodes(ctx)
services, err := tailkit.Node(srv, "vps-1").Docker().Swarm().Services(ctx)
tasks, err    := tailkit.Node(srv, "vps-1").Docker().Swarm().Tasks(ctx)

// availability check — returns false on 503, never errors
available, err := tailkit.Node(srv, "vps-1").Docker().Available(ctx)
Systemd
units, err   := tailkit.Node(srv, "vps-1").Systemd().Units(ctx)
unit, err    := tailkit.Node(srv, "vps-1").Systemd().Unit(ctx, "nginx.service")
file, err    := tailkit.Node(srv, "vps-1").Systemd().UnitFile(ctx, "nginx.service")
job, err     := tailkit.Node(srv, "vps-1").Systemd().Start(ctx, "nginx.service")
job, err      = tailkit.Node(srv, "vps-1").Systemd().Stop(ctx, "nginx.service")
job, err      = tailkit.Node(srv, "vps-1").Systemd().Restart(ctx, "nginx.service")
job, err      = tailkit.Node(srv, "vps-1").Systemd().Reload(ctx, "nginx.service")
job, err      = tailkit.Node(srv, "vps-1").Systemd().Enable(ctx, "nginx.service")
job, err      = tailkit.Node(srv, "vps-1").Systemd().Disable(ctx, "nginx.service")
entries, err := tailkit.Node(srv, "vps-1").Systemd().Journal(ctx, "nginx.service", 100)
entries, err  = tailkit.Node(srv, "vps-1").Systemd().SystemJournal(ctx, 100)
available, _  := tailkit.Node(srv, "vps-1").Systemd().Available(ctx)
Metrics
host, err      := tailkit.Node(srv, "vps-1").Metrics().Host(ctx)
cpu, err       := tailkit.Node(srv, "vps-1").Metrics().CPU(ctx)
memory, err    := tailkit.Node(srv, "vps-1").Metrics().Memory(ctx)
disks, err     := tailkit.Node(srv, "vps-1").Metrics().Disk(ctx)
network, err   := tailkit.Node(srv, "vps-1").Metrics().Network(ctx)
processes, err := tailkit.Node(srv, "vps-1").Metrics().Processes(ctx)
all, err       := tailkit.Node(srv, "vps-1").Metrics().All(ctx)
available, _   := tailkit.Node(srv, "vps-1").Metrics().Available(ctx)

Fleet client

tailkit.AllNodes discovers all online peers via lc.Status(), fans out requests concurrently with bounded parallelism, and returns results and errors per node. One node failing never aborts the whole fan-out.

// same metric across every online node
cpuByNode, errs := tailkit.AllNodes(srv).Metrics().CPU(ctx)
// returns map[nodeName]cpu.Result, map[nodeName]error

// discover nodes with a specific tool installed
nodes, err := tailkit.Discover(ctx, srv, "devbox")
// returns []NodeInfo — node name, tailscale IP, and the matching Tool entry

// broadcast a file to all nodes with a matching receiver configured
results, err := tailkit.Broadcast(ctx, srv, tailkit.SendRequest{
    LocalPath: "/home/user/nginx/api.conf",
    DestPath:  "/etc/nginx/conf.d/api.conf",
})
// returns []SendResult — one per node, errors collected not propagated

// push a var to all nodes that have the scope in their vars.toml
err = tailkit.AllNodes(srv).Vars("myapp", "prod").Set(ctx, "LOG_LEVEL", "debug")

Logging

tailkit itself does not log. It is a library — logging decisions belong to the tool that imports it.

When debugging a tool built on tailkit, enable tailkitd's development logs on the target node to see the full request lifecycle including permission checks, exec invocations, and integration responses:

# on the target node — restart tailkitd with development logging
TAILKITD_ENV=development systemctl restart tailkitd

# follow logs
journalctl -u tailkitd -f

tailkitd logs carry a component field per integration and a caller field showing which node made the request, making it straightforward to isolate traffic from a specific tool during development.


Context conventions

Every tailkit function that performs I/O accepts context.Context as its first parameter. This is standard Go convention and is consistently applied throughout the library — no exceptions.

// all client methods follow this signature
tools, err := tailkit.Node(srv, "vps-1").Tools(ctx)
val,   err := tailkit.Node(srv, "vps-1").Vars("myapp", "prod").Get(ctx, "KEY")
err         = tailkit.Install(ctx, tool)

Pass a context with a deadline for any operation that should not block indefinitely:

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
containers, err := tailkit.Node(srv, "vps-1").Docker().Containers(ctx)

Fleet operations respect per-node timeouts. If one node times out, its error is recorded in the error map and the fan-out continues to the remaining nodes — the timeout of one node never blocks results from others:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cpuByNode, errs := tailkit.AllNodes(srv).Metrics().CPU(ctx)
// nodes that responded within 5s are in cpuByNode
// nodes that timed out are in errs — other nodes are unaffected

Async job polling via ExecWait uses the caller's context as the polling deadline. Cancelling the context stops polling but does not cancel the running job on the remote node — the job runs to completion (or its declared timeout) regardless.


Error types

tailkit.ErrReceiveNotConfigured  // node has no files.toml
tailkit.ErrToolNotFound          // tool not installed on node
tailkit.ErrCommandNotFound       // command not registered by tool
tailkit.ErrDockerUnavailable     // node has no docker.toml or daemon not running
tailkit.ErrSystemdUnavailable    // node has no systemd.toml or D-Bus unavailable
tailkit.ErrMetricsUnavailable    // node has no metrics.toml
tailkit.ErrVarScopeNotFound      // project/env scope not in vars.toml
tailkit.ErrPermissionDenied      // ACL cap or node config blocked the operation

Check errors:

result, err := tailkit.Node(srv, "vps-1").Send(ctx, req)
if errors.Is(err, tailkit.ErrReceiveNotConfigured) {
    // skip this node — not configured for file receive
}

Shared types

tailkit imports the following type packages — the same ones tailkitd uses to encode responses. No translation layer between server and client.

github.com/docker/docker/api/types/container
github.com/docker/docker/api/types/image
github.com/docker/docker/api/types/swarm
github.com/docker/docker/api/types/network
github.com/docker/docker/api/types/volume
github.com/coreos/go-systemd/v22/dbus
github.com/shirou/gopsutil/v4/cpu
github.com/shirou/gopsutil/v4/mem
github.com/shirou/gopsutil/v4/disk
github.com/shirou/gopsutil/v4/net
github.com/shirou/gopsutil/v4/host

Module path

github.com/wf-pro-dev/tailkit

Documentation

Index

Constants

View Source
const (
	// PatternIdentifier matches container names, service names, etc.
	PatternIdentifier = `^[a-zA-Z0-9_-]+$`
	// PatternPath matches absolute unix paths.
	PatternPath = `^(/[a-zA-Z0-9_./-]+)+$`
	// PatternSemver matches semantic version strings.
	PatternSemver = `^v?[0-9]+\.[0-9]+\.[0-9]+$`
	// PatternIP matches IPv4 addresses.
	PatternIP = `^(\d{1,3}\.){3}\d{1,3}$`
	// PatternPort matches valid port numbers.
	PatternPort = `^([1-9][0-9]{0,4})$`
	// PatternFilename matches safe filenames.
	PatternFilename = `^[a-zA-Z0-9_.,-]+$`
)

Variables

View Source
var (
	ErrReceiveNotConfigured = errors.New("tailkit: node has no files.toml (receive not configured)")
	ErrToolNotFound         = errors.New("tailkit: tool not installed on node")
	ErrCommandNotFound      = errors.New("tailkit: command not registered by tool")
	ErrDockerUnavailable    = errors.New("tailkit: node has no docker.toml or daemon not running")
	ErrSystemdUnavailable   = errors.New("tailkit: node has no systemd.toml or D-Bus unavailable")
	ErrMetricsUnavailable   = errors.New("tailkit: node has no metrics.toml")
	ErrVarScopeNotFound     = errors.New("tailkit: project/env scope not in vars.toml")
	ErrPermissionDenied     = errors.New("tailkit: ACL cap or node config blocked the operation")
)

Functions

func AuthMiddleware

func AuthMiddleware(srv *Server) func(http.Handler) http.Handler

AuthMiddleware authenticates every inbound request via Tailscale's WhoIs API.

func ExecWith

func ExecWith(ctx context.Context, vars map[string]string, argv []string) error

ExecWith injects vars into the environment of a local subprocess and runs it. Vars are set as KEY=VALUE environment variables. The subprocess inherits the current process's environment with the vars overlaid on top.

Secrets exist only in the child process environment and disappear when it exits — they are never written to disk.

Example:

vars, err := tailkit.Node(srv, "vps-1").Vars("myapp", "prod").List(ctx)
err = tailkit.ExecWith(ctx, vars, []string{"/usr/bin/node", "server.js"})

func Install

func Install(ctx context.Context, tool Tool) error

Install writes a Tool registration file to /etc/tailkitd/tools/{name}.json.

Call Install once at install time and again on every tool upgrade. tailkitd reads this file to populate its tool registry and exec command list. The write is atomic — tailkitd will never read a partially-written file.

Install validates:

  • Tool.Name is non-empty and matches [a-zA-Z0-9_-]+
  • Tool.Version is non-empty
  • Each Command.Name is non-empty
  • Each Command.ExecParts is non-empty and ExecParts[0] exists on disk
  • Each Command.Timeout is positive
  • Each Arg.Pattern (if set) is a valid regular expression

It creates /etc/tailkitd/tools/ if it does not exist.

func Uninstall

func Uninstall(name string) error

Uninstall removes the tool registration file for the named tool.

If the file does not exist, Uninstall returns nil — it is safe to call Uninstall when the tool may or may not be installed.

Types

type Arg

type Arg struct {
	// Name is the template variable name used in ExecParts (e.g. "container").
	Name string `json:"name"`
	// Type is the value type: "string", "int", "bool".
	Type string `json:"type"`
	// Required indicates the arg must be supplied by the caller.
	Required bool `json:"required"`
	// Pattern is a regex the value must match before substitution.
	// Use the PatternXxx constants or a custom expression.
	Pattern string `json:"pattern,omitempty"`
}

Arg describes a single parameter accepted by a Command.

type CallerContextKey

type CallerContextKey struct{}

CallerContextKey is the exported context key type for CallerIdentity.

type CallerIdentity

type CallerIdentity struct {
	Hostname    string
	TailscaleIP string
	UserLogin   string
	Caps        map[string]bool
}

CallerIdentity holds the verified identity of the caller on an inbound request.

func CallerFromContext

func CallerFromContext(ctx context.Context) (CallerIdentity, bool)

CallerFromContext retrieves the CallerIdentity injected by AuthMiddleware.

func (CallerIdentity) HasCap

func (id CallerIdentity) HasCap(cap string) bool

HasCap reports whether the caller was granted the given ACL capability.

type ComposeClient

type ComposeClient struct {
	// contains filtered or unexported fields
}

ComposeClient provides access to Docker Compose operations.

func (*ComposeClient) Build

func (cc *ComposeClient) Build(ctx context.Context, name string) (Job, error)

func (*ComposeClient) Down

func (cc *ComposeClient) Down(ctx context.Context, name string) (Job, error)

func (*ComposeClient) Project

func (cc *ComposeClient) Project(ctx context.Context, name string) (map[string]any, error)

func (*ComposeClient) Projects

func (cc *ComposeClient) Projects(ctx context.Context) ([]map[string]any, error)

func (*ComposeClient) Pull

func (cc *ComposeClient) Pull(ctx context.Context, name string) (Job, error)

func (*ComposeClient) Restart

func (cc *ComposeClient) Restart(ctx context.Context, name string) (Job, error)

func (*ComposeClient) Up

func (cc *ComposeClient) Up(ctx context.Context, name, composefile string) (Job, error)

type DirEntry

type DirEntry struct {
	Name    string    `json:"name"`
	Size    int64     `json:"size"`
	IsDir   bool      `json:"is_dir"`
	ModTime time.Time `json:"mod_time"`
	Mode    string    `json:"mode"`
}

DirEntry is a single entry in a directory listing.

type DockerClient

type DockerClient struct {
	// contains filtered or unexported fields
}

DockerClient provides typed access to the /integrations/docker endpoints.

func (*DockerClient) Available

func (dc *DockerClient) Available(ctx context.Context) (bool, error)

Available returns false if Docker is not configured or the daemon is down. Never returns a Go error — callers can use it as a boolean check.

func (*DockerClient) Compose

func (dc *DockerClient) Compose() *ComposeClient

func (*DockerClient) Container

func (dc *DockerClient) Container(ctx context.Context, id string) (map[string]any, error)

func (*DockerClient) Containers

func (dc *DockerClient) Containers(ctx context.Context) ([]map[string]any, error)

func (*DockerClient) Images

func (dc *DockerClient) Images(ctx context.Context) ([]map[string]any, error)

func (*DockerClient) Logs

func (dc *DockerClient) Logs(ctx context.Context, id string, tail int) (string, error)

func (*DockerClient) Pull

func (dc *DockerClient) Pull(ctx context.Context, ref string) (Job, error)

func (*DockerClient) Remove

func (dc *DockerClient) Remove(ctx context.Context, id string) (Job, error)

func (*DockerClient) Restart

func (dc *DockerClient) Restart(ctx context.Context, id string) (Job, error)

func (*DockerClient) Start

func (dc *DockerClient) Start(ctx context.Context, id string) (Job, error)

func (*DockerClient) Stats

func (dc *DockerClient) Stats(ctx context.Context, id string) (map[string]any, error)

func (*DockerClient) Stop

func (dc *DockerClient) Stop(ctx context.Context, id string) (Job, error)

func (*DockerClient) Swarm

func (dc *DockerClient) Swarm() *SwarmClient

type FilesClient

type FilesClient struct {
	// contains filtered or unexported fields
}

FilesClient provides typed access to the /files endpoints on a node. Obtain via NodeClient.Files().

func (*FilesClient) Download

func (fc *FilesClient) Download(ctx context.Context, remotePath, localPath string) error

Download fetches a file from the node and writes it to localPath.

func (*FilesClient) List

func (fc *FilesClient) List(ctx context.Context, dirPath string) ([]DirEntry, error)

List returns the directory listing for path on the node.

func (*FilesClient) Read

func (fc *FilesClient) Read(ctx context.Context, path string) (string, error)

Read returns the content of a file on the node as a string.

type FleetClient

type FleetClient struct {
	// contains filtered or unexported fields
}

FleetClient fans out operations to all online tailkitd nodes. Obtain via tailkit.AllNodes(srv).

func AllNodes

func AllNodes(srv *Server) *FleetClient

AllNodes returns a FleetClient that discovers all online tailkitd peers and fans out requests to them with bounded parallelism (10 concurrent).

func (*FleetClient) Metrics

func (f *FleetClient) Metrics() *FleetMetricsClient

func (*FleetClient) Vars

func (f *FleetClient) Vars(project, env string) *FleetVarsClient

type FleetMetricsClient

type FleetMetricsClient struct {
	// contains filtered or unexported fields
}

FleetMetricsClient fans out metrics requests to all nodes.

func (*FleetMetricsClient) All

func (fm *FleetMetricsClient) All(ctx context.Context) (map[string]map[string]any, map[string]error)

func (*FleetMetricsClient) CPU

func (fm *FleetMetricsClient) CPU(ctx context.Context) (map[string]map[string]any, map[string]error)

func (*FleetMetricsClient) Memory

func (fm *FleetMetricsClient) Memory(ctx context.Context) (map[string]map[string]any, map[string]error)

type FleetVarsClient

type FleetVarsClient struct {
	// contains filtered or unexported fields
}

FleetVarsClient fans out var operations across all nodes.

func (*FleetVarsClient) List

func (fv *FleetVarsClient) List(ctx context.Context) (map[string]map[string]string, map[string]error)

List reads the scope from every node. Nodes where the scope is not configured return ErrVarScopeNotFound in the error map.

func (*FleetVarsClient) Set

func (fv *FleetVarsClient) Set(ctx context.Context, key, value string) map[string]error

Set writes a var to the given scope on every node that has it configured. Errors are collected per-node and returned together — one node failing does not prevent writes to other nodes.

type Job

type Job struct {
	// JobID is the opaque identifier used to poll for results.
	JobID  string    `json:"job_id"`
	Status JobStatus `json:"status"`
}

Job is the immediate response from a fire-and-forget exec invocation.

type JobResult

type JobResult struct {
	JobID      string    `json:"job_id"`
	Status     JobStatus `json:"status"`
	ExitCode   int       `json:"exit_code"`
	Stdout     string    `json:"stdout"`
	Stderr     string    `json:"stderr"`
	DurationMs int64     `json:"duration_ms"`
	// Error is set when the job failed to start (not when the command exits non-zero).
	Error string `json:"error,omitempty"`
}

JobResult is returned when polling a completed job.

type JobStatus

type JobStatus string

JobStatus represents the lifecycle state of an async job.

const (
	JobStatusAccepted  JobStatus = "accepted"
	JobStatusRunning   JobStatus = "running"
	JobStatusCompleted JobStatus = "completed"
	JobStatusFailed    JobStatus = "failed"
	JobStatusCancelled JobStatus = "cancelled"
)

type MetricsClient

type MetricsClient struct {
	// contains filtered or unexported fields
}

MetricsClient provides typed access to the /integrations/metrics endpoints.

func (*MetricsClient) All

func (mc *MetricsClient) All(ctx context.Context) (map[string]any, error)

func (*MetricsClient) Available

func (mc *MetricsClient) Available(ctx context.Context) (bool, error)

func (*MetricsClient) CPU

func (mc *MetricsClient) CPU(ctx context.Context) (map[string]any, error)

func (*MetricsClient) Disk

func (mc *MetricsClient) Disk(ctx context.Context) ([]map[string]any, error)

func (*MetricsClient) Host

func (mc *MetricsClient) Host(ctx context.Context) (map[string]any, error)

func (*MetricsClient) Memory

func (mc *MetricsClient) Memory(ctx context.Context) (map[string]any, error)

func (*MetricsClient) Network

func (mc *MetricsClient) Network(ctx context.Context) ([]map[string]any, error)

func (*MetricsClient) Processes

func (mc *MetricsClient) Processes(ctx context.Context) ([]map[string]any, error)

type NodeClient

type NodeClient struct {
	// contains filtered or unexported fields
}

NodeClient is the entry point for all operations on a single tailkitd node. Obtain one via tailkit.Node(srv, "hostname").

func Node

func Node(srv *Server, hostname string) *NodeClient

Node returns a NodeClient that communicates with the tailkitd instance running on the named node. The hostname is the node's Tailscale hostname (e.g. "warehouse-13-1") — tailkit prepends "tailkitd-" to form the tsnet hostname "tailkitd-warehouse-13-1.<tailnet>.ts.net".

Node construction is free — no network calls are made until a method is called on the returned client or one of its sub-clients.

func (*NodeClient) Docker

func (n *NodeClient) Docker() *DockerClient

func (*NodeClient) ExecJob

func (n *NodeClient) ExecJob(ctx context.Context, jobID string) (JobResult, error)

ExecJob polls for the result of a previously submitted job.

func (*NodeClient) ExecWait

func (n *NodeClient) ExecWait(ctx context.Context, jobID string) (JobResult, error)

ExecWait fires a command and blocks until it completes or ctx is cancelled. Cancelling ctx stops polling but does not cancel the running job on the node.

func (*NodeClient) Files

func (n *NodeClient) Files() *FilesClient

Files returns a FilesClient for this node.

func (*NodeClient) HasTool

func (n *NodeClient) HasTool(ctx context.Context, name, minVersion string) (bool, error)

HasTool reports whether the node has a specific tool installed at or above the given minimum version. An empty minVersion matches any version.

func (*NodeClient) Metrics

func (n *NodeClient) Metrics() *MetricsClient

func (*NodeClient) Send

func (n *NodeClient) Send(ctx context.Context, req SendRequest) (SendResult, error)

Send pushes a local file to the node. Returns a SendResult; if a post_recv hook was triggered, SendResult.JobID is set and can be polled with ExecJob.

func (*NodeClient) SendDir

func (n *NodeClient) SendDir(ctx context.Context, req SendDirRequest) ([]SendResult, error)

SendDir pushes all files in a local directory to the node recursively. Returns one SendResult per file; errors are collected, not propagated.

func (*NodeClient) Systemd

func (n *NodeClient) Systemd() *SystemdClient

func (*NodeClient) Tools

func (n *NodeClient) Tools(ctx context.Context) ([]Tool, error)

Tools returns all tools registered on the node.

func (*NodeClient) Vars

func (n *NodeClient) Vars(project, env string) *VarsClient

Vars returns a VarsClient scoped to project/env.

type NodeInfo

type NodeInfo struct {
	// Name is the Tailscale hostname of the node.
	Name string
	// TailscaleIP is the node's Tailscale IP address (100.x.x.x).
	TailscaleIP string
	// Tool is the matching Tool entry found on the node.
	Tool Tool
}

NodeInfo is returned by Discover — it identifies a tailnet peer that has a specific tool installed.

func Discover

func Discover(ctx context.Context, srv *Server, toolName string) ([]NodeInfo, error)

Discover finds all online tailnet peers that have the named tool installed. An empty minVersion matches any version.

func DiscoverVersion

func DiscoverVersion(ctx context.Context, srv *Server, toolName, minVersion string) ([]NodeInfo, error)

DiscoverVersion is like Discover but requires at least minVersion.

type SendDirRequest

type SendDirRequest struct {
	// LocalDir is the absolute path to the source directory on the caller's machine.
	LocalDir string
	// DestPath is the absolute destination directory path on the node.
	DestPath string
}

SendDirRequest describes a directory tree to push to a remote node.

type SendRequest

type SendRequest struct {
	// LocalPath is the absolute path to the file on the caller's machine.
	LocalPath string
	// DestPath is the absolute path the file should be written to on the node.
	DestPath string
}

SendRequest describes a single file to push to a remote node.

type SendResult

type SendResult struct {
	LocalPath string `json:"local_path"`
	// Success indicates whether the file was successfully sent.
	Success bool `json:"success"`
	// WrittenTo is the absolute path the file was written to on the node.
	WrittenTo string `json:"written_to"`
	// BytesWritten is the number of bytes written.
	BytesWritten int64 `json:"bytes_written"`
	// DestMachine is the hostname of the machine the file was sent to.
	DestMachine string `json:"dest_machine"`

	//Error is set when the file was not successfully sent.
	Error string `json:"error,omitempty"`
}

SendResult is the response from a Send or SendDir operation.

func Broadcast

func Broadcast(ctx context.Context, srv *Server, req SendRequest) ([]SendResult, map[string]error)

Broadcast pushes a file to all online nodes concurrently. Each node that has a matching write rule receives the file. Nodes that are offline or have no matching write rule are skipped — their errors are collected and returned, not propagated.

type Server

type Server struct {
	*tsnet.Server
}

Server is a tailkit-managed tsnet server.

func NewServer

func NewServer(cfg ServerConfig) (*Server, error)

NewServer constructs and starts a tsnet server.

func (*Server) ListenAndServe

func (s *Server) ListenAndServe(addr string, handler http.Handler) error

ListenAndServe starts a plain HTTP server on the tsnet listener.

func (*Server) ListenAndServeTLS

func (s *Server) ListenAndServeTLS(addr string, handler http.Handler) error

ListenAndServeTLS starts an HTTPS server on the tsnet listener.

func (*Server) TLSConfig

func (s *Server) TLSConfig() *tls.Config

TLSConfig returns a *tls.Config using Tailscale-issued certificates.

type ServerConfig

type ServerConfig struct {
	Hostname  string
	AuthKey   string
	StateDir  string
	Ephemeral bool
}

ServerConfig holds configuration for a tailkit-managed tsnet server.

type SwarmClient

type SwarmClient struct {
	// contains filtered or unexported fields
}

SwarmClient provides access to Docker Swarm read operations.

func (*SwarmClient) Nodes

func (sc *SwarmClient) Nodes(ctx context.Context) ([]map[string]any, error)

func (*SwarmClient) Services

func (sc *SwarmClient) Services(ctx context.Context) ([]map[string]any, error)

func (*SwarmClient) Tasks

func (sc *SwarmClient) Tasks(ctx context.Context) ([]map[string]any, error)

type SystemdClient

type SystemdClient struct {
	// contains filtered or unexported fields
}

SystemdClient provides typed access to the /integrations/systemd endpoints.

func (*SystemdClient) Available

func (sc *SystemdClient) Available(ctx context.Context) (bool, error)

func (*SystemdClient) Disable

func (sc *SystemdClient) Disable(ctx context.Context, unit string) (Job, error)

func (*SystemdClient) Enable

func (sc *SystemdClient) Enable(ctx context.Context, unit string) (Job, error)

func (*SystemdClient) Journal

func (sc *SystemdClient) Journal(ctx context.Context, unit string, lines int) ([]map[string]any, error)

func (*SystemdClient) Reload

func (sc *SystemdClient) Reload(ctx context.Context, unit string) (Job, error)

func (*SystemdClient) Restart

func (sc *SystemdClient) Restart(ctx context.Context, unit string) (Job, error)

func (*SystemdClient) Start

func (sc *SystemdClient) Start(ctx context.Context, unit string) (Job, error)

func (*SystemdClient) Stop

func (sc *SystemdClient) Stop(ctx context.Context, unit string) (Job, error)

func (*SystemdClient) SystemJournal

func (sc *SystemdClient) SystemJournal(ctx context.Context, lines int) ([]map[string]any, error)

func (*SystemdClient) Unit

func (sc *SystemdClient) Unit(ctx context.Context, unit string) (map[string]any, error)

func (*SystemdClient) UnitFile

func (sc *SystemdClient) UnitFile(ctx context.Context, unit string) (string, error)

func (*SystemdClient) Units

func (sc *SystemdClient) Units(ctx context.Context) ([]map[string]any, error)

type Tool

type Tool struct {
	// Name is a unique identifier for the tool across the tailnet.
	Name string `json:"name"`
	// Version is the tool's current version string (semver recommended).
	Version string `json:"version"`
	// TsnetHost is the tsnet hostname this tool registers on the tailnet.
	TsnetHost string `json:"tsnet_host"`
}

Tool is the registration record written to /etc/tailkitd/tools/{name}.json by tailkit.Install and read by tailkitd to populate its tool registry.

type VarsClient

type VarsClient struct {
	// contains filtered or unexported fields
}

VarsClient provides typed access to the /vars endpoints on a node.

func (*VarsClient) Delete

func (vc *VarsClient) Delete(ctx context.Context, key string) error

Delete removes a var from the scope.

func (*VarsClient) Env

func (vc *VarsClient) Env(ctx context.Context) (string, error)

Env returns all vars rendered as sorted KEY=VALUE lines suitable for sourcing in a shell script or writing to a .env file.

func (*VarsClient) Get

func (vc *VarsClient) Get(ctx context.Context, key string) (string, error)

Get returns the value of a single var.

func (*VarsClient) List

func (vc *VarsClient) List(ctx context.Context) (map[string]string, error)

List returns all vars in the scope as a map.

func (*VarsClient) Set

func (vc *VarsClient) Set(ctx context.Context, key, value string) error

Set writes a var to the scope.

Jump to

Keyboard shortcuts

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