From e73e23dfccef129ef03cd88cd769dc3ee7037712 Mon Sep 17 00:00:00 2001 From: Nightshift Date: Tue, 21 Apr 2026 20:18:09 +0000 Subject: [PATCH] docs: add Go doc comments to all exported types and functions Add comprehensive doc comments to exported types, functions, and methods across 9 packages: model, app, tailscale, config, crypto, state, platform, gui, and logging. Nightshift docs-backfill task. --- internal/app/workflow.go | 8 ++++++++ internal/config/config.go | 5 +++++ internal/crypto/secret.go | 3 +++ internal/gui/server.go | 2 ++ internal/logging/logger.go | 5 +++++ internal/model/types.go | 13 +++++++++++++ internal/platform/platform.go | 11 +++++++++++ internal/state/store.go | 4 ++++ internal/tailscale/client.go | 12 ++++++++++++ 9 files changed, 63 insertions(+) diff --git a/internal/app/workflow.go b/internal/app/workflow.go index 5ae32ff..61fa9a4 100644 --- a/internal/app/workflow.go +++ b/internal/app/workflow.go @@ -26,6 +26,7 @@ import ( "github.com/tailstick/tailstick/internal/tailscale" ) +// Runtime holds filesystem paths and flags that configure a Manager instance. type Runtime struct { ConfigPath string StatePath string @@ -34,6 +35,7 @@ type Runtime struct { DryRun bool } +// Manager orchestrates lease enrollment, agent scheduling, and cleanup. type Manager struct { Runtime Runtime Logger *logging.Logger @@ -42,6 +44,7 @@ type Manager struct { HostCtx platform.Context } +// NewManager creates a Manager by detecting the host platform, initializing logging, and preparing paths. func NewManager(rt Runtime) (*Manager, error) { host, err := platform.Detect() if err != nil { @@ -79,10 +82,12 @@ func NewManager(rt Runtime) (*Manager, error) { }, nil } +// Close releases the Manager's log file handle. func (m *Manager) Close() { _ = m.Logger.Close() } +// Enroll creates a new lease: validates the preset, installs Tailscale, authenticates, and persists the lease record. func (m *Manager) Enroll(ctx context.Context, opts model.RuntimeOptions) (model.LeaseRecord, error) { if !m.Runtime.DryRun && !platform.IsElevated() { return model.LeaseRecord{}, fmt.Errorf("elevated privileges are required for enrollment; %s", platform.ElevationHint(m.HostCtx.ExePath, nil)) @@ -211,6 +216,7 @@ func (m *Manager) Enroll(ctx context.Context, opts model.RuntimeOptions) (model. return rec, nil } +// AgentOnce performs a single reconciliation pass over all leases, cleaning up expired ones. func (m *Manager) AgentOnce(ctx context.Context) error { st, err := state.Load(m.Runtime.StatePath) if err != nil { @@ -252,6 +258,7 @@ func (m *Manager) AgentOnce(ctx context.Context) error { return nil } +// AgentRun starts a long-running agent loop that periodically calls AgentOnce until no managed leases remain. func (m *Manager) AgentRun(ctx context.Context, interval time.Duration) error { if interval <= 0 { interval = 1 * time.Minute @@ -280,6 +287,7 @@ func (m *Manager) AgentRun(ctx context.Context, interval time.Duration) error { } } +// ForceCleanup immediately cleans up a specific lease by ID, regardless of its expiry status. func (m *Manager) ForceCleanup(ctx context.Context, leaseID string) error { if strings.TrimSpace(leaseID) == "" { return fmt.Errorf("lease id is required") diff --git a/internal/config/config.go b/internal/config/config.go index b608e46..b222b18 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -18,6 +18,7 @@ const ( DefaultConfigFile = "tailstick.config.json" ) +// Load reads and parses the configuration file, expanding environment variables and validating presets. func Load(path string) (model.Config, error) { if path == "" { path = DefaultConfigFile @@ -43,6 +44,7 @@ func Load(path string) (model.Config, error) { return cfg, nil } +// Validate checks that the configuration has at least one preset with valid auth material. func Validate(cfg model.Config) error { if len(cfg.Presets) == 0 { return errors.New("config must define at least one preset") @@ -68,6 +70,7 @@ func Validate(cfg model.Config) error { return nil } +// FindPreset returns the preset matching the given ID, the default preset, or the first preset. func FindPreset(cfg model.Config, id string) (model.Preset, error) { if id == "" { id = cfg.DefaultPreset @@ -83,6 +86,7 @@ func FindPreset(cfg model.Config, id string) (model.Preset, error) { return model.Preset{}, fmt.Errorf("preset %q not found", id) } +// ResolvePath joins a relative path to a base directory, returning absolute paths unchanged. func ResolvePath(baseDir, path string) string { if path == "" { return "" @@ -93,6 +97,7 @@ func ResolvePath(baseDir, path string) string { return filepath.Join(baseDir, path) } +// ResolvePresetSecrets fills in preset auth keys and API keys from environment variables when inline values are empty. func ResolvePresetSecrets(p model.Preset) model.Preset { out := p if strings.TrimSpace(out.AuthKey) == "" && strings.TrimSpace(out.AuthKeyEnv) != "" { diff --git a/internal/crypto/secret.go b/internal/crypto/secret.go index 18d24bd..366f36a 100644 --- a/internal/crypto/secret.go +++ b/internal/crypto/secret.go @@ -17,6 +17,7 @@ import ( "golang.org/x/crypto/scrypt" ) +// Envelope holds the encrypted payload and metadata for AES-GCM sealed secrets. type Envelope struct { Mode string `json:"mode"` Salt string `json:"salt"` @@ -24,6 +25,7 @@ type Envelope struct { Cipher string `json:"cipher"` } +// Encrypt encrypts plaintext using AES-GCM with a scrypt-derived key bound to the machine or a password. func Encrypt(plain, password, machineContext string) (string, error) { key, salt, mode, err := deriveKey(password, machineContext) if err != nil { @@ -55,6 +57,7 @@ func Encrypt(plain, password, machineContext string) (string, error) { return base64.StdEncoding.EncodeToString(b), nil } +// Decrypt decodes and decrypts a base64-encoded Envelope back to plaintext. func Decrypt(encoded, password, machineContext string) (string, error) { raw, err := base64.StdEncoding.DecodeString(encoded) if err != nil { diff --git a/internal/gui/server.go b/internal/gui/server.go index 0a11573..02285da 100644 --- a/internal/gui/server.go +++ b/internal/gui/server.go @@ -24,6 +24,7 @@ import ( //go:embed index.html tailstick-favicon.png var staticFS embed.FS +// Server is the GUI HTTP server providing preset listing and enrollment endpoints. type Server struct { ConfigPath string Logf func(format string, args ...any) @@ -62,6 +63,7 @@ var validChannels = map[string]bool{ string(model.ChannelLatest): true, } +// Run starts the GUI HTTP server on the given host:port, optionally opening a browser. func Run(ctx context.Context, srv *Server, openBrowser bool, host string, port int) error { host = strings.TrimSpace(host) if host == "" { diff --git a/internal/logging/logger.go b/internal/logging/logger.go index e28ebf8..607f290 100644 --- a/internal/logging/logger.go +++ b/internal/logging/logger.go @@ -12,12 +12,14 @@ import ( "time" ) +// Logger provides thread-safe, leveled logging to both a file and stdout. type Logger struct { mu sync.Mutex file *os.File std *log.Logger } +// New creates a Logger that appends to the given file path. func New(path string) (*Logger, error) { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return nil, err @@ -32,6 +34,7 @@ func New(path string) (*Logger, error) { }, nil } +// Close flushes and closes the underlying log file. func (l *Logger) Close() error { l.mu.Lock() defer l.mu.Unlock() @@ -43,10 +46,12 @@ func (l *Logger) Close() error { return err } +// Info logs a message at INFO level. func (l *Logger) Info(format string, args ...any) { l.write("INFO", format, args...) } +// Error logs a message at ERROR level. func (l *Logger) Error(format string, args ...any) { l.write("ERROR", format, args...) } diff --git a/internal/model/types.go b/internal/model/types.go index 224d835..355cd9f 100644 --- a/internal/model/types.go +++ b/internal/model/types.go @@ -4,6 +4,7 @@ package model import "time" +// LeaseMode represents the lifecycle policy for a Tailscale device lease. type LeaseMode string const ( @@ -12,6 +13,7 @@ const ( LeaseModePermanent LeaseMode = "permanent" ) +// Channel selects between stable and latest Tailscale release tracks. type Channel string const ( @@ -19,6 +21,7 @@ const ( ChannelLatest Channel = "latest" ) +// LeaseStatus tracks the current phase of a lease in its lifecycle. type LeaseStatus string const ( @@ -28,6 +31,7 @@ const ( LeaseStatusCleaned LeaseStatus = "cleaned" ) +// Config is the top-level tailstick configuration loaded from tailstick.config.json. type Config struct { StableVersion string `json:"stableVersion"` DefaultPreset string `json:"defaultPreset"` @@ -36,6 +40,7 @@ type Config struct { Presets []Preset `json:"presets"` } +// Preset defines a named enrollment profile with auth keys, tags, and install commands. type Preset struct { ID string `json:"id"` Description string `json:"description"` @@ -52,6 +57,7 @@ type Preset struct { Cleanup Cleanup `json:"cleanup"` } +// Install holds platform-specific install and uninstall command templates. type Install struct { LinuxStable []string `json:"linuxStable"` LinuxLatest []string `json:"linuxLatest"` @@ -61,6 +67,7 @@ type Install struct { WindowsUninstall []string `json:"windowsUninstall"` } +// Cleanup configures post-lease device removal via the Tailscale API. type Cleanup struct { Tailnet string `json:"tailnet"` APIKey string `json:"apiKey"` @@ -68,6 +75,7 @@ type Cleanup struct { DeviceDeleteEnabled bool `json:"deviceDeleteEnabled"` } +// RuntimeOptions carries the user-supplied parameters for a single enrollment operation. type RuntimeOptions struct { PresetID string Mode LeaseMode @@ -81,6 +89,7 @@ type RuntimeOptions struct { Password string } +// LeaseRecord is the persistent record of a single lease, stored in state.json. type LeaseRecord struct { LeaseID string `json:"leaseId"` PresetID string `json:"presetId"` @@ -104,12 +113,14 @@ type LeaseRecord struct { EncryptedSecret string `json:"encryptedSecret,omitempty"` } +// LocalState is the on-disk state file containing all lease records. type LocalState struct { SchemaVersion int `json:"schemaVersion"` UpdatedAt time.Time `json:"updatedAt"` Records []LeaseRecord `json:"records"` } +// AuditEntry represents a single line in the NDJSON audit log. type AuditEntry struct { Timestamp time.Time `json:"timestamp"` LeaseID string `json:"leaseId"` @@ -122,12 +133,14 @@ type AuditEntry struct { Message string `json:"message"` } +// TailscaleSelf holds identity fields from the local Tailscale node. type TailscaleSelf struct { ID string `json:"ID"` DNSName string `json:"DNSName"` HostName string `json:"HostName"` } +// TailscaleStatus is the parsed output of "tailscale status --json". type TailscaleStatus struct { Self TailscaleSelf `json:"Self"` } diff --git a/internal/platform/platform.go b/internal/platform/platform.go index 1d6dc1c..2713453 100644 --- a/internal/platform/platform.go +++ b/internal/platform/platform.go @@ -1,3 +1,4 @@ +// Package platform provides host detection, privilege checks, and platform-specific path helpers for tailstick. package platform import ( @@ -11,6 +12,7 @@ import ( "strings" ) +// Context holds detected host properties: OS, architecture, hostname, boot ID, and executable path. type Context struct { OS string Arch string @@ -19,6 +21,7 @@ type Context struct { ExePath string } +// Detect collects the current host Context by reading system information. func Detect() (Context, error) { host, err := os.Hostname() if err != nil { @@ -42,6 +45,7 @@ func Detect() (Context, error) { func IsLinux() bool { return runtime.GOOS == "linux" } func IsWindows() bool { return runtime.GOOS == "windows" } +// StatePath returns the platform-specific path for the lease state file. func StatePath() string { if IsWindows() { root := os.Getenv("ProgramData") @@ -53,6 +57,7 @@ func StatePath() string { return "/var/lib/tailstick/state.json" } +// LogPath returns the platform-specific path for the log file. func LogPath() string { if IsWindows() { root := os.Getenv("ProgramData") @@ -64,6 +69,7 @@ func LogPath() string { return "/var/log/tailstick.log" } +// LocalSecretPath returns the platform-specific directory for encrypted lease secrets. func LocalSecretPath() string { if IsWindows() { root := os.Getenv("ProgramData") @@ -75,6 +81,7 @@ func LocalSecretPath() string { return "/var/lib/tailstick/secrets" } +// AgentBinaryPath returns the platform-specific path where the agent binary is installed. func AgentBinaryPath() string { if IsWindows() { root := os.Getenv("ProgramData") @@ -86,6 +93,7 @@ func AgentBinaryPath() string { return "/var/lib/tailstick/tailstick-agent" } +// EnsureParent creates the parent directory of the given path if it does not exist. func EnsureParent(path string) error { parent := filepath.Dir(path) if err := os.MkdirAll(parent, 0o755); err != nil { @@ -137,6 +145,7 @@ func sanitizeHost(in string) string { return strings.Trim(string(out), "-") } +// RequireSupportedLinux returns an error if the current Linux distro is not Debian or Ubuntu. func RequireSupportedLinux() error { if !IsLinux() { return nil @@ -152,6 +161,7 @@ func RequireSupportedLinux() error { return errors.New("linux target unsupported: only debian/ubuntu are supported in v1") } +// IsElevated returns true if the current process has administrative/root privileges. func IsElevated() bool { if IsLinux() { return os.Geteuid() == 0 @@ -164,6 +174,7 @@ func IsElevated() bool { return true } +// ElevationHint returns a human-readable suggestion for gaining elevated privileges. func ElevationHint(exePath string, args []string) string { if IsLinux() { return "rerun with sudo" diff --git a/internal/state/store.go b/internal/state/store.go index c1ad5f6..0e58490 100644 --- a/internal/state/store.go +++ b/internal/state/store.go @@ -13,6 +13,7 @@ import ( "github.com/tailstick/tailstick/internal/model" ) +// Load reads the state file, returning an empty state if the file does not exist. func Load(path string) (model.LocalState, error) { b, err := os.ReadFile(path) if err != nil { @@ -34,6 +35,7 @@ func Load(path string) (model.LocalState, error) { return st, nil } +// Save atomically writes the state to disk via a temporary file rename. func Save(path string, st model.LocalState) error { st.SchemaVersion = 1 st.UpdatedAt = time.Now().UTC() @@ -52,6 +54,7 @@ func Save(path string, st model.LocalState) error { return os.Rename(tmp, path) } +// UpsertRecord inserts a new lease record or updates an existing one with the same LeaseID. func UpsertRecord(st model.LocalState, rec model.LeaseRecord) model.LocalState { for i := range st.Records { if st.Records[i].LeaseID == rec.LeaseID { @@ -63,6 +66,7 @@ func UpsertRecord(st model.LocalState, rec model.LeaseRecord) model.LocalState { return st } +// AppendAudit appends a timestamped NDJSON audit entry to the audit log file. func AppendAudit(path string, entry model.AuditEntry) error { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return err diff --git a/internal/tailscale/client.go b/internal/tailscale/client.go index 288eb8f..f705307 100644 --- a/internal/tailscale/client.go +++ b/internal/tailscale/client.go @@ -18,17 +18,20 @@ import ( "github.com/tailstick/tailstick/internal/platform" ) +// Client wraps Tailscale CLI operations through a platform.Runner for testability. type Client struct { Runner platform.Runner } var defaultDeleteDeviceHTTPClient = &http.Client{Timeout: 15 * time.Second} +// IsInstalled checks whether the Tailscale CLI is available on the system. func (c Client) IsInstalled(ctx context.Context) bool { _, err := c.Runner.Run(ctx, []string{"tailscale", "version"}) return err == nil } +// EnsureInstalled installs Tailscale if not present, then pins the version for the stable channel. func (c Client) EnsureInstalled(ctx context.Context, preset model.Preset, channel model.Channel, stableVersion string) error { if !c.IsInstalled(ctx) { cmd := installCommand(preset, channel) @@ -52,6 +55,7 @@ func (c Client) EnsureInstalled(ctx context.Context, preset model.Preset, channe return nil } +// Up runs "tailscale up" with auth key, hostname, tags, routes, and optional exit node. func (c Client) Up(ctx context.Context, preset model.Preset, deviceName string, mode model.LeaseMode, exitNode string) error { auth := preset.AuthKey if mode == model.LeaseModeSession { @@ -89,16 +93,19 @@ func (c Client) Up(ctx context.Context, preset model.Preset, deviceName string, return err } +// Down runs "tailscale down" to disconnect from the tailnet. func (c Client) Down(ctx context.Context) error { _, err := c.Runner.Run(ctx, []string{"tailscale", "down"}) return err } +// Logout runs "tailscale logout" to remove local node credentials. func (c Client) Logout(ctx context.Context) error { _, err := c.Runner.Run(ctx, []string{"tailscale", "logout"}) return err } +// Status queries the current Tailscale status, with a fallback for schema drift. func (c Client) Status(ctx context.Context) (model.TailscaleStatus, error) { out, err := c.Runner.Run(ctx, []string{"tailscale", "status", "--json"}) if err != nil { @@ -131,6 +138,7 @@ func (c Client) Status(ctx context.Context) (model.TailscaleStatus, error) { return st, nil } +// Uninstall removes Tailscale using the preset's configured uninstall command. func (c Client) Uninstall(ctx context.Context, preset model.Preset) error { cmd := uninstallCommand(preset) if len(cmd) == 0 { @@ -140,6 +148,7 @@ func (c Client) Uninstall(ctx context.Context, preset model.Preset) error { return err } +// DeleteDevice removes a device from the tailnet via the Tailscale API. func DeleteDevice(ctx context.Context, apiKey, deviceID string) error { return deleteDevice(ctx, defaultDeleteDeviceHTTPClient, apiKey, deviceID) } @@ -227,6 +236,7 @@ func uninstallCommand(preset model.Preset) []string { return []string{"bash", "-lc", "apt-get remove -y tailscale"} } +// BuildMachineContext assembles a machine-specific context string used for secret key derivation. func BuildMachineContext(host, _ string) string { info := []string{runtime.GOOS, runtime.GOARCH, strings.ToLower(strings.TrimSpace(host))} if runtime.GOOS == "linux" { @@ -237,6 +247,7 @@ func BuildMachineContext(host, _ string) string { return strings.Join(info, "|") } +// ParseDurationDays validates and returns the lease duration in days for the given mode. func ParseDurationDays(mode model.LeaseMode, defaultDays, customDays int) (int, error) { switch mode { case model.LeaseModeSession: @@ -261,6 +272,7 @@ func ParseDurationDays(mode model.LeaseMode, defaultDays, customDays int) (int, } } +// Future returns a time pointer set to the given number of days from now, or nil if days <= 0. func Future(ts time.Time, days int) *time.Time { if days <= 0 { return nil