Skip to content
Closed
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
1 change: 1 addition & 0 deletions internal/app/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions internal/app/gui.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
1 change: 1 addition & 0 deletions internal/app/workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand All @@ -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
Expand All @@ -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 ""
Expand All @@ -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) != "" {
Expand Down
2 changes: 2 additions & 0 deletions internal/crypto/secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
5 changes: 3 additions & 2 deletions internal/gui/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 == "" {
Expand Down Expand Up @@ -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 != "/" {
Expand All @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions internal/logging/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions internal/platform/platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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")
Expand All @@ -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")
Expand All @@ -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")
Expand All @@ -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")
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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"
Expand Down
4 changes: 4 additions & 0 deletions internal/state/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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()
Expand All @@ -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 {
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions internal/tailscale/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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" {
Expand All @@ -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:
Expand All @@ -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
Expand Down