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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ dist/

# test binary, built with `go test -c`
*.test
*-test.yaml

# output of the go coverage tool, specifically when used with LiteIDE
*.out
Expand Down
44 changes: 34 additions & 10 deletions cmd/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import (
"bufio"
"fmt"
"os"
"strings"

"github.com/borisdvlpr/gotail/internal/config"
ierror "github.com/borisdvlpr/gotail/internal/error"
"github.com/borisdvlpr/gotail/internal/file"
"github.com/borisdvlpr/gotail/internal/format"
"github.com/borisdvlpr/gotail/internal/input"
"github.com/borisdvlpr/gotail/internal/system"
"github.com/spf13/afero"
Expand All @@ -35,9 +35,8 @@ func NewSetupCmd(deps SetupCommand) *cobra.Command {
Short: "Setup Tailscale on a new device",
RunE: func(cmd *cobra.Command, args []string) (err error) {
flags := []string{"tailscale", "up", "--ssh"}
initConfig := []string{
"runcmd:\n",
` - [ sh, -c, curl -fsSL https://tailscale.com/install.sh | sh ]` + "\n",
runcmds := [][]string{
{"sh", "-c", "curl -fsSL https://tailscale.com/install.sh | sh"},
}
cfg := &config.Config{}

Expand All @@ -62,7 +61,6 @@ func NewSetupCmd(deps SetupCommand) *cobra.Command {
if err = yaml.Unmarshal(configFile, &cfg); err != nil {
return err
}

} else {
cfg.ExitNode, err = input.PromptUser("Setup device as an exit node?", []string{"y", "n"})
if err != nil {
Expand All @@ -86,6 +84,11 @@ func NewSetupCmd(deps SetupCommand) *cobra.Command {
return err
}

cfg.Tags, err = input.PromptUser("Please enter tags for this device (comma separated):", nil)
if err != nil {
return err
}

cfg.AuthKey, err = input.PromptUser("Please enter your Tailscale authkey:", nil)
if err != nil {
return err
Expand All @@ -106,19 +109,41 @@ func NewSetupCmd(deps SetupCommand) *cobra.Command {
return err
}

initConfig = append(initConfig, ` - [ sh, -c, echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf && echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf && sudo sysctl -p /etc/sysctl.d/99-tailscale.conf ]`+"\n")
runcmds = append(runcmds, []string{
"sh", "-c",
"echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf && " +
"echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf && " +
"sudo sysctl -p /etc/sysctl.d/99-tailscale.conf",
})
flags = append(flags, fmt.Sprintf("--advertise-routes=%s", cfg.Subnets))
}

if cfg.Hostname != "" {
flags = append(flags, fmt.Sprintf("--hostname=%s", cfg.Hostname))
initConfig = append(initConfig, fmt.Sprintf(` - [ sh, -c, sudo hostnamectl hostname %s ]`+"\n", cfg.Hostname))
runcmds = append(runcmds, []string{
"sh", "-c", fmt.Sprintf("sudo hostnamectl hostname %s", cfg.Hostname),
})
}

if cfg.Tags != "" {
normalized, tagErr := input.ValidateTags(cfg.Tags)
if tagErr != nil {
return tagErr
}
cfg.Tags = normalized
flags = append(flags, fmt.Sprintf("--advertise-tags=%s", cfg.Tags))
cmd.Printf("This device will be tagged: %s.\n", cfg.Tags)
}

flags = append(flags, fmt.Sprintf("--authkey=%s", cfg.AuthKey))
runcmds = append(runcmds, flags)

cmd.Println("Adding Tailscale to 'user-data' file.")

initConfig = append(initConfig, fmt.Sprintf(" - [ %s ]\n", strings.Join(flags, ", ")))
initConfig := []string{"runcmd:\n"}
for _, rc := range runcmds {
initConfig = append(initConfig, " - "+format.BuildRunCmd(rc)+"\n")
}

initFile, err := deps.Fsys.OpenFile(filePath, os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
Expand All @@ -134,8 +159,7 @@ func NewSetupCmd(deps SetupCommand) *cobra.Command {

writer := bufio.NewWriter(initFile)
for _, conf := range initConfig {
_, err = writer.WriteString(conf)
if err != nil {
if _, err = writer.WriteString(conf); err != nil {
return err
}
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func TestSetup_Success(t *testing.T) {
contentBytes, _ := afero.ReadFile(mockFS, userDataPath)
content := string(contentBytes)

if !strings.Contains(content, "- [ sh, -c, sudo hostnamectl hostname raspberrypi ]") {
if !strings.Contains(content, `- ["sh", "-c", "sudo hostnamectl hostname raspberrypi"]`) {
t.Errorf("Expected user-data to contain hostname 'raspberrypi', got:\n%s", content)
}
}
Expand Down
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@ require (
github.com/spf13/afero v1.15.0
github.com/spf13/cobra v1.10.2
github.com/spf13/viper v1.21.0
golang.org/x/sys v0.44.0
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/sys v0.47.0
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/fsnotify/fsnotify v1.10.1 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.3.1 // indirect
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/text v0.40.0 // indirect
)
12 changes: 6 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
Expand All @@ -41,10 +41,10 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
9 changes: 5 additions & 4 deletions internal/config/config.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
package config

import ierr "github.com/borisdvlpr/gotail/internal/error"
import ierror "github.com/borisdvlpr/gotail/internal/error"

type Config struct {
ExitNode string `yaml:"exit_node"`
SubnetRouter string `yaml:"subnet_router"`
Subnets string `yaml:"subnets"`
Hostname string `yaml:"hostname"`
Tags string `yaml:"tags"`
AuthKey string `yaml:"auth_key"`
}

func (c *Config) Validate() error {
if c.AuthKey == "" {
return ierr.StatusError{Status: "auth key is required", StatusCode: 1}
return ierror.StatusError{Status: "auth key is required", StatusCode: 1}
}

if c.SubnetRouter == "y" && c.Subnets == "" {
return ierr.StatusError{Status: "subnets are required when subnet router is enabled", StatusCode: 1}
return ierror.StatusError{Status: "subnets are required when subnet router is enabled", StatusCode: 1}
}

if c.Hostname == "" {
return ierr.StatusError{Status: "hostname is required", StatusCode: 1}
return ierror.StatusError{Status: "hostname is required", StatusCode: 1}
}

return nil
Expand Down
37 changes: 26 additions & 11 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"errors"
"testing"

ierr "github.com/borisdvlpr/gotail/internal/error"
ierror "github.com/borisdvlpr/gotail/internal/error"
)

type ValidateConfigTestCase struct {
Expand All @@ -16,40 +16,55 @@ type ValidateConfigTestCase struct {
func TestConfigValidate(t *testing.T) {
testCases := []ValidateConfigTestCase{
{
id: "case_01",
id: "exit_node_valid",
config: Config{ExitNode: "y", SubnetRouter: "n", Hostname: "test-host", AuthKey: "tskey-abcd1234"},
expectedError: nil,
},
{
id: "case_02",
id: "subnet_router_valid",
config: Config{ExitNode: "n", SubnetRouter: "y", Subnets: "192.168.1.0/24", Hostname: "subnet-router", AuthKey: "tskey-abcd1234"},
expectedError: nil,
},
{
id: "case_03",
id: "missing_auth_key",
config: Config{ExitNode: "y", SubnetRouter: "n", Hostname: "test-host", AuthKey: ""},
expectedError: ierr.StatusError{Status: "auth key is required", StatusCode: 1},
expectedError: ierror.StatusError{Status: "auth key is required", StatusCode: 1},
},
{
id: "case_04",
id: "missing_hostname",
config: Config{ExitNode: "y", SubnetRouter: "n", Hostname: "", AuthKey: "tskey-abcd1234"},
expectedError: ierr.StatusError{Status: "hostname is required", StatusCode: 1},
expectedError: ierror.StatusError{Status: "hostname is required", StatusCode: 1},
},
{
id: "case_05",
id: "subnet_router_without_subnets",
config: Config{ExitNode: "n", SubnetRouter: "y", Subnets: "", Hostname: "subnet-router", AuthKey: "tskey-abcd1234"},
expectedError: ierr.StatusError{Status: "subnets are required when subnet router is enabled", StatusCode: 1},
expectedError: ierror.StatusError{Status: "subnets are required when subnet router is enabled", StatusCode: 1},
},
{
id: "case_06",
id: "simple_node_valid",
config: Config{ExitNode: "n", SubnetRouter: "n", Hostname: "simple-node", AuthKey: "tskey-abcd1234"},
expectedError: nil,
},
{
id: "case_07",
id: "fully_configured_valid",
config: Config{ExitNode: "y", SubnetRouter: "y", Subnets: "192.168.1.0/24,10.0.0.0/16", Hostname: "fully-configured-node", AuthKey: "tskey-abcdefghijklmnop"},
expectedError: nil,
},
{
id: "tags_valid",
config: Config{ExitNode: "n", SubnetRouter: "n", Hostname: "tagged-node", Tags: "server,prod", AuthKey: "tskey-abcd1234"},
expectedError: nil,
},
{
id: "tags_do_not_mask_missing_auth_key",
config: Config{ExitNode: "n", SubnetRouter: "n", Hostname: "tagged-node", Tags: "server", AuthKey: ""},
expectedError: ierror.StatusError{Status: "auth key is required", StatusCode: 1},
},
{
id: "tags_with_full_config_valid",
config: Config{ExitNode: "y", SubnetRouter: "y", Subnets: "192.168.1.0/24,10.0.0.0/16", Hostname: "full-node", Tags: "exit,router", AuthKey: "tskey-abcd1234"},
expectedError: nil,
},
}

for _, tc := range testCases {
Expand Down
6 changes: 4 additions & 2 deletions internal/file/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
"github.com/spf13/afero"
)

var errFound = errors.New("file found")

// DriveSearcher is the platform-specific strategy for locating a file across
// the drives or mountpoints visible on the current OS. Each platform provides
// its own implementation via NewSystemSearcher; an unsupported operating system
Expand Down Expand Up @@ -62,13 +64,13 @@ func GetFilePath(fsys afero.Fs, rootDir string, fileName string) (string, error)

if !info.IsDir() && info.Name() == fileName {
foundPath = path
return filepath.SkipDir
return errFound
}

return nil
})

if err != nil && !errors.Is(err, filepath.SkipDir) {
if err != nil && !errors.Is(err, errFound) {
return "", fmt.Errorf("%w", err)
}

Expand Down
4 changes: 2 additions & 2 deletions internal/file/file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ type GetFilePathTestCase struct {
func TestGetFilePath(t *testing.T) {
testCases := []GetFilePathTestCase{
{
id: "case_01",
id: "valid_path",
file: "user-data",
expectedPath: filepath.Join("file-test-dir", "user-data"),
},
{
id: "case_02",
id: "empty_path",
file: "",
expectedPath: "",
},
Expand Down
35 changes: 28 additions & 7 deletions internal/file/linux_utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,34 @@ type IsMountpointSearchableTestCase struct {

func TestIsMountpointSearchable(t *testing.T) {
testCases := []IsMountpointSearchableTestCase{
{id: "valid_media", mountpoint: "/media/usb", expected: true},
{id: "valid_mnt", mountpoint: "/mnt/data", expected: true},
{id: "valid_run_media", mountpoint: "/run/media/user/disk", expected: true},
{id: "root", mountpoint: "/", expected: false},
{id: "empty", mountpoint: "", expected: false},
{id: "invalid_prefix", mountpoint: "/home/user", expected: false},
{id: "invalid_chars", mountpoint: "/mnt/\x00bad", expected: false},
{
id: "valid_media",
mountpoint: "/media/usb",
expected: true},
{
id: "valid_mnt",
mountpoint: "/mnt/data",
expected: true},
{
id: "valid_run_media",
mountpoint: "/run/media/user/disk",
expected: true},
{
id: "root",
mountpoint: "/",
expected: false},
{
id: "empty",
mountpoint: "",
expected: false},
{
id: "invalid_prefix",
mountpoint: "/home/user",
expected: false},
{
id: "invalid_chars",
mountpoint: "/mnt/\x00bad",
expected: false},
}

for _, tc := range testCases {
Expand Down
Loading
Loading