diff --git a/internal/app/cli.go b/internal/app/cli.go index 066e6e4..c07841a 100644 --- a/internal/app/cli.go +++ b/internal/app/cli.go @@ -15,6 +15,7 @@ import ( const Version = "1.0.0" +// RunCLI dispatches the CLI subcommand and returns an exit code. func RunCLI(args []string, rt Runtime) int { if len(args) == 0 { return runEnroll(args, rt) diff --git a/internal/app/gui.go b/internal/app/gui.go index d0001af..e39f7b0 100644 --- a/internal/app/gui.go +++ b/internal/app/gui.go @@ -8,6 +8,7 @@ import ( "github.com/tailstick/tailstick/internal/gui" ) +// RunGUI starts the web UI server for interactive lease creation. func RunGUI(args []string, rt Runtime) int { fs := flag.NewFlagSet("gui", flag.ContinueOnError) var ( diff --git a/internal/app/workflow.go b/internal/app/workflow.go index d8a4d5e..e11afee 100644 --- a/internal/app/workflow.go +++ b/internal/app/workflow.go @@ -39,6 +39,7 @@ type Manager struct { HostCtx platform.Context } +// NewManager creates a Manager initialized with the given runtime configuration. func NewManager(rt Runtime) (*Manager, error) { host, err := platform.Detect() if err != nil { diff --git a/internal/config/config.go b/internal/config/config.go index 0ee9c3f..bbc142f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -15,6 +15,7 @@ const ( DefaultConfigFile = "tailstick.config.json" ) +// Load reads and parses the YAML config file. func Load(path string) (model.Config, error) { if path == "" { path = DefaultConfigFile @@ -40,6 +41,7 @@ func Load(path string) (model.Config, error) { return cfg, nil } +// Validate checks config for required fields and logical consistency. func Validate(cfg model.Config) error { if len(cfg.Presets) == 0 { return errors.New("config must define at least one preset") @@ -65,6 +67,7 @@ func Validate(cfg model.Config) error { return nil } +// FindPreset returns a preset by ID, or an error if not found. func FindPreset(cfg model.Config, id string) (model.Preset, error) { if id == "" { id = cfg.DefaultPreset @@ -80,6 +83,7 @@ func FindPreset(cfg model.Config, id string) (model.Preset, error) { return model.Preset{}, fmt.Errorf("preset %q not found", id) } +// ResolvePath expands ~ and resolves relative paths against a base directory. func ResolvePath(baseDir, path string) string { if path == "" { return "" @@ -90,6 +94,7 @@ func ResolvePath(baseDir, path string) string { return filepath.Join(baseDir, path) } +// ResolvePresetSecrets decrypts encrypted preset secrets using the local key. 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 4989bed..1b578b4 100644 --- a/internal/crypto/secret.go +++ b/internal/crypto/secret.go @@ -21,6 +21,7 @@ type Envelope struct { Cipher string `json:"cipher"` } +// Encrypt encrypts plaintext using AES-GCM with the configured key. func Encrypt(plain, password, machineContext string) (string, error) { key, salt, mode, err := deriveKey(password, machineContext) if err != nil { @@ -52,6 +53,7 @@ func Encrypt(plain, password, machineContext string) (string, error) { return base64.StdEncoding.EncodeToString(b), nil } +// Decrypt decrypts an AES-GCM ciphertext produced by Encrypt. 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 737d494..fa4e072 100644 --- a/internal/gui/server.go +++ b/internal/gui/server.go @@ -39,6 +39,7 @@ type enrollRequest struct { Password string `json:"password"` } +// Run starts the HTTP server for the GUI, serving the embedded UI and API endpoints. func Run(ctx context.Context, srv *Server, openBrowser bool, host string, port int) error { host = strings.TrimSpace(host) if host == "" { @@ -134,7 +135,7 @@ func (s *Server) index(w http.ResponseWriter, r *http.Request) { b, _ := staticFS.ReadFile("tailstick-favicon.png") w.Header().Set("Content-Type", "image/png") w.Header().Set("Cache-Control", "public, max-age=86400") - w.Write(b) + _, _ = w.Write(b) return } if r.URL.Path != "/" { @@ -143,7 +144,7 @@ func (s *Server) index(w http.ResponseWriter, r *http.Request) { } b, _ := staticFS.ReadFile("index.html") w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write(b) + _, _ = w.Write(b) } func writeJSON(w http.ResponseWriter, data any) { diff --git a/internal/logging/logger.go b/internal/logging/logger.go index f04e05f..8dd250c 100644 --- a/internal/logging/logger.go +++ b/internal/logging/logger.go @@ -15,6 +15,7 @@ type Logger struct { std *log.Logger } +// New creates a leveled logger that writes to the specified log file. func New(path string) (*Logger, error) { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return nil, err diff --git a/internal/platform/platform.go b/internal/platform/platform.go index 1d6dc1c..c5b77b2 100644 --- a/internal/platform/platform.go +++ b/internal/platform/platform.go @@ -19,6 +19,7 @@ type Context struct { ExePath string } +// Detect returns the current platform identifier. func Detect() (Context, error) { host, err := os.Hostname() if err != nil { @@ -39,9 +40,12 @@ func Detect() (Context, error) { return ctx, nil } +// IsLinux reports whether the current OS is Linux. func IsLinux() bool { return runtime.GOOS == "linux" } +// IsWindows reports whether the current OS is Windows. func IsWindows() bool { return runtime.GOOS == "windows" } +// StatePath returns the default path for the 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 default 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 default path for the local secret key. func LocalSecretPath() string { if IsWindows() { root := os.Getenv("ProgramData") @@ -75,6 +81,7 @@ func LocalSecretPath() string { return "/var/lib/tailstick/secrets" } +// AgentBinaryPath returns the default path for the agent binary. 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 panics if the OS is not a supported Linux distribution. 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 reports whether the process is running with elevated privileges. func IsElevated() bool { if IsLinux() { return os.Geteuid() == 0 @@ -164,6 +174,7 @@ func IsElevated() bool { return true } +// ElevationHint returns a platform-specific suggestion for obtaining 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 0015e43..dc318fc 100644 --- a/internal/state/store.go +++ b/internal/state/store.go @@ -10,6 +10,7 @@ import ( "github.com/tailstick/tailstick/internal/model" ) +// Load reads and parses the state file into a Store. func Load(path string) (model.LocalState, error) { b, err := os.ReadFile(path) if err != nil { @@ -31,6 +32,7 @@ func Load(path string) (model.LocalState, error) { return st, nil } +// Save writes the current state to disk. func Save(path string, st model.LocalState) error { st.SchemaVersion = 1 st.UpdatedAt = time.Now().UTC() @@ -49,6 +51,7 @@ func Save(path string, st model.LocalState) error { return os.Rename(tmp, path) } +// UpsertRecord adds or updates a lease record by LeaseID. func UpsertRecord(st model.LocalState, rec model.LeaseRecord) model.LocalState { for i := range st.Records { if st.Records[i].LeaseID == rec.LeaseID { @@ -60,6 +63,7 @@ func UpsertRecord(st model.LocalState, rec model.LeaseRecord) model.LocalState { return st } +// AppendAudit appends an 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 1ea245d..8bd9287 100644 --- a/internal/tailscale/client.go +++ b/internal/tailscale/client.go @@ -131,6 +131,7 @@ func (c Client) Uninstall(ctx context.Context, preset model.Preset) error { return err } +// DeleteDevice removes a device from the Tailscale tailnet using the API. func DeleteDevice(ctx context.Context, apiKey, deviceID string) error { if strings.TrimSpace(apiKey) == "" || strings.TrimSpace(deviceID) == "" { return nil @@ -188,6 +189,7 @@ func uninstallCommand(preset model.Preset) []string { return []string{"bash", "-lc", "apt-get remove -y tailscale"} } +// BuildMachineContext constructs a stable machine identifier string from OS, arch, hostname, and machine-id. func BuildMachineContext(host, _ string) string { info := []string{runtime.GOOS, runtime.GOARCH, strings.ToLower(strings.TrimSpace(host))} if runtime.GOOS == "linux" { @@ -198,6 +200,7 @@ func BuildMachineContext(host, _ string) string { return strings.Join(info, "|") } +// ParseDurationDays validates and returns the lease duration in days based on the mode. func ParseDurationDays(mode model.LeaseMode, defaultDays, customDays int) (int, error) { switch mode { case model.LeaseModeSession: @@ -222,6 +225,7 @@ func ParseDurationDays(mode model.LeaseMode, defaultDays, customDays int) (int, } } +// Future returns a pointer to the time that is days after ts, or nil if days is non-positive. func Future(ts time.Time, days int) *time.Time { if days <= 0 { return nil