Server: - Add a --cache-path allowlist so a server can be limited to the caches it is meant to purge, defaulting to any path as before. - Set socket permissions explicitly (--socket-mode, default 0660) instead of inheriting the service manager's umask, which left the socket unreachable. - Refuse to remove a socket another instance is still serving. - Read keys both raw and decoded, so keys nginx stored with escapes and keys a caller escaped by hand both purge. - Add an exact= parameter for literal keys containing glob punctuation. - Report purge failures as 500 rather than 502, and send error bodies through http.Error so a failure is not reported as a successful purge. - Graceful shutdown with systemd readiness notification. Purge: - Group purge arguments into PurgeRequest and report the number of entries removed. - Compile exclude globs once, and fail the purge when one is invalid rather than purging the keys it was meant to keep. - Cap header scanning and tolerate entries nginx evicts mid-walk. - Switch to filepath.WalkDir to avoid an Lstat per cache file. New: - service command to install, start, stop, and remove the system service. - service install takes --cache-path, writing the allowlist into the unit it installs, so an installed service is restricted from its first start. - Makefile, VERSION, and build identifiers stamped via ldflags. - Tests for the server handler and the service command. Build: - Update to Go 1.25, kong v1, GoReleaser v2, and current GitHub Actions. - Add vet and test steps to CI. - Rename purgeCmd.go/serverCmd.go to Go's file naming convention. Bump version to 0.2.0.
121 lines
3.8 KiB
Go
121 lines
3.8 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/alecthomas/kong"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// The allowlist has to reach the installed unit's ExecStart, as that is the
|
|
// only place the server it starts reads it from.
|
|
func TestServiceArguments(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
cachePaths []string
|
|
want []string
|
|
}{
|
|
{
|
|
// What the command has always installed, so a service installed
|
|
// without the flag runs exactly as it did before.
|
|
name: "no allowlist",
|
|
want: []string{"server"},
|
|
},
|
|
{
|
|
name: "one cache path",
|
|
cachePaths: []string{"/var/nginx/proxy_temp/cache"},
|
|
want: []string{"server", "--cache-path", "/var/nginx/proxy_temp/cache"},
|
|
},
|
|
{
|
|
// The flag repeats, and each one has to arrive as its own argument
|
|
// rather than joined into a value the server reads as one path.
|
|
name: "several cache paths",
|
|
cachePaths: []string{"/var/cache/one", "/var/cache/two"},
|
|
want: []string{
|
|
"server",
|
|
"--cache-path", "/var/cache/one",
|
|
"--cache-path", "/var/cache/two",
|
|
},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
cmd := &ServiceCmd{Action: "install", CachePaths: test.cachePaths}
|
|
require.Equal(t, test.want, cmd.arguments())
|
|
|
|
// The same arguments have to be what the service definition is
|
|
// built with, or the unit is written without them.
|
|
svc, err := cmd.service()
|
|
require.NoError(t, err)
|
|
require.NotNil(t, svc)
|
|
})
|
|
}
|
|
}
|
|
|
|
// The unit runs from a working directory of the service manager's choosing, so
|
|
// a relative path would name a different directory there than the one the
|
|
// install was typed against.
|
|
func TestServiceArgumentsAbsolutePaths(t *testing.T) {
|
|
cmd := &ServiceCmd{Action: "install", CachePaths: []string{"cache"}}
|
|
|
|
arguments := cmd.arguments()
|
|
|
|
require.Len(t, arguments, 3)
|
|
require.True(t, filepath.IsAbs(arguments[2]), "%s is not absolute", arguments[2])
|
|
require.Equal(t, "cache", filepath.Base(arguments[2]))
|
|
}
|
|
|
|
// A symlinked cache is resolved per request by the server, so the install must
|
|
// leave the link alone rather than pin the allowlist to today's target.
|
|
func TestServiceArgumentsKeepsSymlinks(t *testing.T) {
|
|
base := t.TempDir()
|
|
target := filepath.Join(base, "cache")
|
|
link := filepath.Join(base, "link")
|
|
require.NoError(t, os.MkdirAll(target, 0o755))
|
|
require.NoError(t, os.Symlink(target, link))
|
|
|
|
cmd := &ServiceCmd{Action: "install", CachePaths: []string{link}}
|
|
|
|
require.Equal(t, []string{"server", "--cache-path", link}, cmd.arguments())
|
|
}
|
|
|
|
// The allowlist is written into the unit at install, so accepting it on an
|
|
// action that cannot act on it would read as having changed the allowlist of a
|
|
// service that carries on with the one it was installed with.
|
|
func TestServiceRejectsCachePathsOutsideInstall(t *testing.T) {
|
|
for _, action := range ServiceAction {
|
|
if action == "install" {
|
|
continue
|
|
}
|
|
t.Run(action, func(t *testing.T) {
|
|
cmd := &ServiceCmd{Action: action, CachePaths: []string{t.TempDir()}}
|
|
|
|
err := cmd.Run()
|
|
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "--cache-path")
|
|
})
|
|
}
|
|
}
|
|
|
|
// The flag has to be reachable from the command line it is documented on, and
|
|
// kong's path type has to expand each value rather than only the first.
|
|
func TestServiceCachePathFlagParses(t *testing.T) {
|
|
flags := &Flags{}
|
|
parser, err := kong.New(flags, kong.Name(Name), kongVars())
|
|
require.NoError(t, err)
|
|
|
|
_, err = parser.Parse([]string{
|
|
"service", "install",
|
|
"--cache-path", "/var/cache/one",
|
|
"--cache-path", "relative/cache",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, "install", flags.Service.Action)
|
|
require.Len(t, flags.Service.CachePaths, 2)
|
|
require.Equal(t, "/var/cache/one", flags.Service.CachePaths[0])
|
|
require.True(t, filepath.IsAbs(flags.Service.CachePaths[1]), "relative path was not expanded")
|
|
}
|