.template-docker: &template-docker
before_script:
- apt-get update
- - apt-get install --no-install-recommends --yes build-essential ca-certificates git golang golang-golang-x-tools make openssh-server shellcheck
+ - apt-get install --no-install-recommends --yes build-essential ca-certificates git make openssh-server shellcheck
script:
# Gitlab-runner uses umask 0000 (wtf?!) and mixes nobody and root user
# when setting up the environment. This breaks ssh's permission check on
- chown -R root:root /builds
- chmod -R go-w /builds
#
- - mkdir /run/sshd
- ./ci/run
# Windows is not really supported, but at least check building works
- PATH=$HOME/go/bin:$PATH make GOOS=windows GOFLAGS=
-debian-sid:
+debian:
<<: *template-docker
- image: debian:sid
+ image: golang:1.24-trixie
+version: "2"
+
linters:
- disable-all: true
+ default: none
enable:
# Enabled by default
- - deadcode
- errcheck
- - gosimple
- govet
- ineffassign
- staticcheck
- - structcheck
- - typecheck
- unused
- - varcheck
# Additional checks
- bodyclose
+ - containedctx
- contextcheck
+ - copyloopvar
- durationcheck
- errname
- exhaustive
- - exportloopref
- - gofmt
+ - exptostd
+ - gocheckcompilerdirectives
+ - gocritic
+ - iface
+ - importas
- nilerr
+ - nilnesserr
- nolintlint
+ - nonamedreturns
+ - nosprintfhostport
- predeclared
+ - reassign
+ - recvcheck
- rowserrcheck
+ - thelper
+ - tparallel
- unconvert
+ - usestdlibvars
+ - usetesting
- wastedassign
- issues:
- # Don't hide potential important issues
- exclude-use-default: false
+ settings:
+ exhaustive:
+ # "default" is good enough to be exhaustive
+ default-signifies-exhaustive: true
+ gocritic:
+ disabled-checks:
+ - exitAfterDefer
+ - ifElseChain
+ - singleCaseSwitch
+ staticcheck:
+ checks:
+ # Defaults
+ - "all"
+ - "-ST1000"
+ - "-ST1003"
+ - "-ST1016"
+ - "-ST1020"
+ - "-ST1021"
+ - "-ST1022"
+ #
+ - "-QF1001"
+ - "-QF1003"
+ - "-QF1003"
+ - "-QF1007"
+ usestdlibvars:
+ http-method: false
+
-linters-settings:
- exhaustive:
- # "default" is good enough to be exhaustive
- default-signifies-exhaustive: true
+run:
+ timeout: 10m
# Additional static checks only run in CI
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
-go install honnef.co/go/tools/cmd/staticcheck@v0.4.6
-staticcheck ./...
-go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2
+go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.5.0
golangci-lint run
test -z "$(git clean -nd)" # any untracked files left?
}
func TestLoadFiles(t *testing.T) {
- cwd, err := os.Getwd()
- if err != nil {
- t.Fatal(err)
- }
- defer os.Chdir(cwd) //nolint:errcheck
-
- err = os.Chdir("../testdata/project")
- if err != nil {
- t.Fatal(err)
- }
+ t.Chdir("../testdata/project")
// Regular users cannot create sticky files
skipInvalidSticky := os.Getuid() != 0 &&
}
ft.CreateFifo("files-invalid-type/files/invalid", 0644)
- defer os.Remove("files-invalid-type/files/invalid")
+ defer os.Remove("files-invalid-type/files/invalid") //nolint:errcheck
const errMsg = `
import (
"fmt"
- "os"
- "path/filepath"
"testing"
"ruderich.org/simon/safcm/testutil"
)
func TestLoadGroups(t *testing.T) {
- cwd, err := os.Getwd()
- if err != nil {
- t.Fatal(err)
- }
- defer os.Chdir(cwd) //nolint:errcheck
-
- err = os.Chdir("../testdata/project")
- if err != nil {
- t.Fatal(err)
- }
- hosts, err := LoadHosts()
- if err != nil {
- t.Fatal(err)
- }
- err = os.Chdir(cwd)
- if err != nil {
- t.Fatal(err)
- }
+ var hosts *Hosts
+ t.Run("load hosts", func(t *testing.T) {
+ t.Chdir("../testdata/project")
+ x, err := LoadHosts()
+ if err != nil {
+ t.Fatal(err)
+ }
+ hosts = x
+ })
tests := []struct {
path string
for _, tc := range tests {
t.Run(tc.path, func(t *testing.T) {
- err := os.Chdir(filepath.Join(cwd, tc.path))
- if err != nil {
- t.Fatal(err)
- }
-
+ t.Chdir(tc.path)
res, err := LoadGroups(tc.cfg, tc.hosts)
testutil.AssertEqual(t, "res", res, tc.exp)
testutil.AssertErrorEqual(t, "err", err, tc.expErr)
}
func TestResolveHostGroups(t *testing.T) {
- cwd, err := os.Getwd()
- if err != nil {
- t.Fatal(err)
- }
- defer os.Chdir(cwd) //nolint:errcheck
+ t.Chdir("../testdata/project")
- err = os.Chdir("../testdata/project")
- if err != nil {
- t.Fatal(err)
- }
allHosts, err := LoadHosts()
if err != nil {
t.Fatal(err)
import (
"fmt"
- "os"
- "path/filepath"
"testing"
"ruderich.org/simon/safcm/testutil"
)
func TestLoadHosts(t *testing.T) {
- cwd, err := os.Getwd()
- if err != nil {
- t.Fatal(err)
- }
- defer os.Chdir(cwd) //nolint:errcheck
-
sliceToHosts := func(hosts []*Host) *Hosts {
res := &Hosts{
List: hosts,
for _, tc := range tests {
t.Run(tc.path, func(t *testing.T) {
- err := os.Chdir(filepath.Join(cwd, tc.path))
- if err != nil {
- t.Fatal(err)
- }
-
+ t.Chdir(tc.path)
res, err := LoadHosts()
testutil.AssertEqual(t, "res", res, tc.exp)
testutil.AssertErrorEqual(t, "err", err, tc.expErr)
import (
"fmt"
"io/fs"
- "os"
"testing"
"ruderich.org/simon/safcm"
)
func TestLoadPermissions(t *testing.T) {
- cwd, err := os.Getwd()
- if err != nil {
- t.Fatal(err)
- }
- defer os.Chdir(cwd) //nolint:errcheck
-
- err = os.Chdir("../testdata/project")
- if err != nil {
- t.Fatal(err)
- }
+ t.Chdir("../testdata/project")
tests := []struct {
group string
import (
"fmt"
"io/fs"
- "os"
"testing"
"ruderich.org/simon/safcm"
)
func TestLoadTemplates(t *testing.T) {
- cwd, err := os.Getwd()
- if err != nil {
- t.Fatal(err)
- }
- defer os.Chdir(cwd) //nolint:errcheck
-
- err = os.Chdir("../testdata/project")
- if err != nil {
- t.Fatal(err)
- }
+ t.Chdir("../testdata/project")
allHosts, err := LoadHosts()
if err != nil {
import (
"fmt"
"io/fs"
- "os"
"testing"
"ruderich.org/simon/safcm"
)
func TestLoadTriggers(t *testing.T) {
- cwd, err := os.Getwd()
- if err != nil {
- t.Fatal(err)
- }
- defer os.Chdir(cwd) //nolint:errcheck
-
- err = os.Chdir("../testdata/project")
- if err != nil {
- t.Fatal(err)
- }
+ t.Chdir("../testdata/project")
tests := []struct {
group string
if err != nil {
return err
}
- defer x.Close()
+ defer x.Close() //nolint:errcheck
err = x.Chmod(mode)
if err != nil {
if err != nil {
t.Fatal(err)
}
- defer os.Chdir(cwd) //nolint:errcheck
var suffix string
// Needs different options in sshd_config
for i := 0; i < 30; i++ {
conn, err := net.Dial("tcp", "127.0.0.1:29327")
if err == nil {
- conn.Close()
+ conn.Close() //nolint:errcheck
break
}
time.Sleep(time.Second)
}
- err = os.Chdir(sshDir + "/project")
- if err != nil {
- t.Fatal(err)
- }
+ t.Chdir(sshDir + "/project")
ft.CreateDirectoryExists("no-changes.example.org", 0755)
ft.CreateDirectoryExists("no-changes.example.org/files", 0755)
t.Run("error before connection is established", func(t *testing.T) {
// Fake $PATH so safcm cannot find the `ssh` binary.
- path := os.Getenv("PATH")
- os.Setenv("PATH", "")
- defer os.Setenv("PATH", path)
+ t.Setenv("PATH", "")
cmd := exec.Command("../../../../../safcm",
"sync", "-n", "no-settings.example.org")
- _, err := cmd.CombinedOutput()
+ _, err = cmd.CombinedOutput()
if err == nil {
t.Errorf("err = nil")
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if tc.remove {
- os.Remove(remotePath)
+ _ = os.Remove(remotePath)
}
args := append([]string{"sync",
})
}
- os.Remove(remotePath)
+ _ = os.Remove(remotePath)
}
)
func TestHostSyncReq(t *testing.T) {
- cwd, err := os.Getwd()
- if err != nil {
- t.Fatal(err)
- }
- defer os.Chdir(cwd) //nolint:errcheck
-
tests := []struct {
name string
project string
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
- err = os.Chdir(filepath.Join(cwd,
- "testdata", tc.project))
- if err != nil {
- t.Fatal(err)
- }
+ t.Chdir(filepath.Join("testdata", tc.project))
// `safcm fixperms` in case user has strict umask
log.SetOutput(io.Discard)
import (
"fmt"
- "os"
"testing"
"ruderich.org/simon/safcm/cmd/safcm/config"
)
func TestHostsToSync(t *testing.T) {
- cwd, err := os.Getwd()
- if err != nil {
- t.Fatal(err)
- }
- defer os.Chdir(cwd) //nolint:errcheck
-
- err = os.Chdir("testdata/project")
- if err != nil {
- t.Fatal(err)
- }
+ t.Chdir("testdata/project")
_, allHosts, allGroups, err := LoadBaseFiles()
if err != nil {
t.Fatal(err)
// Sync all hosts concurrently
var wg sync.WaitGroup
for _, x := range hosts {
- x := x
-
// Once in sync.Host() and once in the go func below
wg.Add(2)
module ruderich.org/simon/safcm
-go 1.16
+go 1.24.0
require (
- github.com/google/go-cmp v0.5.5
- github.com/ianbruene/go-difflib v1.2.0
- golang.org/x/sys v0.0.0-20210608053332-aa57babbf139
- golang.org/x/term v0.0.0-20210317153231-de623e64d2a6
+ github.com/google/go-cmp v0.7.0
+ github.com/ianbruene/go-difflib v1.3.0
+ golang.org/x/sys v0.37.0
+ golang.org/x/term v0.36.0
gopkg.in/yaml.v2 v2.4.0
)
-github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
-github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/ianbruene/go-difflib v1.2.0 h1:iARmgaCq6nW5QptdoFm0PYAyNGix3xw/xRgEwphJSZw=
-github.com/ianbruene/go-difflib v1.2.0/go.mod h1:uJbrQ06VPxjRiRIrync+E6VcWFGW2dWqw2gvQp6HQPY=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210608053332-aa57babbf139 h1:C+AwYEtBp/VQwoLntUmQ/yx3MS9vmZaKNdw5eOpoQe8=
-golang.org/x/sys v0.0.0-20210608053332-aa57babbf139/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 h1:EC6+IGYTjPpRfv9a2b/6Puw0W+hLtAhkV1tPsXhutqs=
-golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/ianbruene/go-difflib v1.3.0 h1:bAz13YotoralrvYAuRGdU6TX/GPYvTjqO2ybVUgw+Bk=
+github.com/ianbruene/go-difflib v1.3.0/go.mod h1:uJbrQ06VPxjRiRIrync+E6VcWFGW2dWqw2gvQp6HQPY=
+golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
+golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
+golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
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.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
if err != nil {
return nil, err
}
- defer unix.Close(parentFd)
+ defer unix.Close(parentFd) //nolint:errcheck
var changes []string
if err != nil {
return nil, nil, err
}
- defer fh.Close()
+ defer fh.Close() //nolint:errcheck
stat, err := fh.Stat()
if err != nil {
"fmt"
"io/fs"
"os"
- "path/filepath"
"runtime"
"syscall"
"testing"
)
func TestHandle(t *testing.T) {
- cwd, err := os.Getwd()
- if err != nil {
- t.Fatal(err)
- }
- defer os.Chdir(cwd) //nolint:errcheck
-
- err = os.RemoveAll("testdata")
- if err != nil {
- t.Fatal(err)
- }
- err = os.Mkdir("testdata", 0700)
- if err != nil {
- t.Fatal(err)
- }
-
// Set umask to test mode for new files
umask := syscall.Umask(027)
defer syscall.Umask(umask)
},
},
nil,
- fmt.Errorf(symlinkExists),
+ fmt.Errorf("%s", symlinkExists),
},
{
"exists: fifo",
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Create separate test directory for each test case
- path := filepath.Join(cwd, "testdata", tc.name)
- err = os.Mkdir(path, 0700)
- if err != nil {
- t.Fatal(err)
- }
- err = os.Chdir(path)
+ path := t.TempDir()
+ err := os.Chmod(path, 0700)
if err != nil {
t.Fatal(err)
}
+ t.Chdir(path)
if tc.prepare != nil {
tc.prepare()
testutil.AssertEqual(t, "files", files, tc.expFiles)
})
}
-
- if !t.Failed() {
- err = os.RemoveAll(filepath.Join(cwd, "testdata"))
- if err != nil {
- t.Fatal(err)
- }
- }
}
"sort"
"strconv"
"strings"
- "time"
"github.com/ianbruene/go-difflib/difflib"
"golang.org/x/sys/unix"
const openReadonlyFlags = unix.O_RDONLY | unix.O_NOFOLLOW | unix.O_NONBLOCK
func (s *Sync) syncFiles() error {
- // To create random file names for symlinks
- rand.Seed(time.Now().UnixNano())
-
// Sort for deterministic order and so parent directories are present
// when files in them are created
var files []*safcm.File
}
return err
}
- defer unix.Close(parentFd)
+ defer unix.Close(parentFd) //nolint:errcheck
var oldStat unix.Stat_t
reopen:
return err
}
} else {
- defer oldFh.Close()
+ defer oldFh.Close() //nolint:errcheck
err := unix.Fstat(int(oldFh.Fd()), &oldStat)
if err != nil {
if err2 == unix.ENOTDIR {
return err
} else if err2 == unix.ENOTEMPTY {
- return fmt.Errorf(msg)
+ return fmt.Errorf("%s", msg)
} else {
return err2
}
if err != nil {
return err
}
- defer dh.Close()
+ defer dh.Close() //nolint:errcheck
err = dh.Chmod(file.Mode)
if err != nil {
for i, name := range parts[:len(parts)-1] {
fd, err := unix.Openat(dirFd, name, openReadonlyFlags, 0)
if err != nil {
- unix.Close(dirFd)
+ unix.Close(dirFd) //nolint:errcheck
if err == unix.ELOOP || err == unix.EMLINK {
x := filepath.Join(append([]string{dir},
parts[:i+1]...)...)
}
return -1, "", err
}
- unix.Close(dirFd)
+ err = unix.Close(dirFd)
+ if err != nil {
+ return -1, "", err
+ }
dirFd = fd
}
if err != nil {
return nil, err
}
- defer unix.Close(parentFd)
+ defer unix.Close(parentFd) //nolint:errcheck
return OpenAtNoFollow(parentFd, baseName)
}
_, err = fh.Write(data)
if err != nil {
- fh.Close()
+ fh.Close() //nolint:errcheck
unix.Unlinkat(dirFd, tmpBase, 0 /* flags */) //nolint:errcheck
return "", err
}
// createTempAt() creates the file with 0600
err = fh.Chown(uid, gid)
if err != nil {
- fh.Close()
+ fh.Close() //nolint:errcheck
unix.Unlinkat(dirFd, tmpBase, 0) //nolint:errcheck
return "", err
}
err = fh.Chmod(mode)
if err != nil {
- fh.Close()
+ fh.Close() //nolint:errcheck
unix.Unlinkat(dirFd, tmpBase, 0) //nolint:errcheck
return "", err
}
err = fh.Sync()
if err != nil {
- fh.Close()
+ fh.Close() //nolint:errcheck
unix.Unlinkat(dirFd, tmpBase, 0) //nolint:errcheck
return "", err
}
"io/fs"
"math/rand"
"os"
- "path/filepath"
"regexp"
"testing"
var randFilesRegexp = regexp.MustCompile(`\d+"$`)
func TestSyncFiles(t *testing.T) {
- cwd, err := os.Getwd()
- if err != nil {
- t.Fatal(err)
- }
- defer os.Chdir(cwd) //nolint:errcheck
-
- err = os.RemoveAll("testdata")
- if err != nil {
- t.Fatal(err)
- }
- err = os.Mkdir("testdata", 0700)
- if err != nil {
- t.Fatal(err)
- }
-
root := ft.File{
Path: ".",
Mode: fs.ModeDir | 0700,
}
// Create separate test directory for each test case
- path := filepath.Join(cwd, "testdata", "files-"+tc.name)
- err := os.Mkdir(path, 0700)
- if err != nil {
- t.Fatal(err)
- }
- err = os.Chdir(path)
+ path := t.TempDir()
+ err := os.Chmod(path, 0700)
if err != nil {
t.Fatal(err)
}
+ t.Chdir(path)
if tc.prepare != nil {
tc.prepare()
})
}
- os.Remove(tmpTestFilePath)
- if !t.Failed() {
- err = os.RemoveAll(filepath.Join(cwd, "testdata"))
- if err != nil {
- t.Fatal(err)
- }
- }
+ _ = os.Remove(tmpTestFilePath)
}
func TestSyncFile(t *testing.T) {
- cwd, err := os.Getwd()
- if err != nil {
- t.Fatal(err)
- }
- defer os.Chdir(cwd) //nolint:errcheck
-
- err = os.RemoveAll("testdata")
- if err != nil {
- t.Fatal(err)
- }
- err = os.Mkdir("testdata", 0700)
- if err != nil {
- t.Fatal(err)
- }
-
root := ft.File{
Path: ".",
Mode: fs.ModeDir | 0700,
`4: files: "link" (group): will create`,
`3: files: "link" (group): creating`,
`4: files: "link" (group): creating temporary symlink ".linkRND"`,
- `4: files: "link" (group): creating temporary symlink ".linkRND"`,
`4: files: "link" (group): renaming ".linkRND"`,
},
nil,
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Create separate test directory for each test case
- path := filepath.Join(cwd, "testdata", "file-"+tc.name)
- err := os.Mkdir(path, 0700)
- if err != nil {
- t.Fatal(err)
- }
- err = os.Chdir(path)
+ path := t.TempDir()
+ err := os.Chmod(path, 0700)
if err != nil {
t.Fatal(err)
}
+ t.Chdir(path)
if tc.prepare != nil {
tc.prepare()
}
// Deterministic temporary symlink names
- rand.Seed(0)
+ rand.Seed(0) //nolint:staticcheck // SA1019 need fixed seed
var changed bool
err = s.syncFile(tc.file, &changed)
testutil.AssertEqual(t, "resp", s.resp, tc.expResp)
})
}
-
- if !t.Failed() {
- err = os.RemoveAll(filepath.Join(cwd, "testdata"))
- if err != nil {
- t.Fatal(err)
- }
- }
}