From 61578369d974e8eceb2a0dd3580e98a0d8ff0cfd Mon Sep 17 00:00:00 2001 From: "Michael (Parker) Parker" Date: Wed, 14 Jan 2026 08:38:26 -0500 Subject: [PATCH 01/15] Start disk quota package --- go.mod | 1 + go.sum | 2 + server/filesystem/quotas/exfs.go | 41 +++++++++++ server/filesystem/quotas/syscall_quota.go | 25 +++++++ server/filesystem/quotas/syscall_xattr.go | 85 +++++++++++++++++++++++ 5 files changed, 154 insertions(+) create mode 100644 server/filesystem/quotas/exfs.go create mode 100644 server/filesystem/quotas/syscall_quota.go create mode 100644 server/filesystem/quotas/syscall_xattr.go diff --git a/go.mod b/go.mod index 55510973..d26d44f7 100644 --- a/go.mod +++ b/go.mod @@ -63,6 +63,7 @@ require ( github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/g0rbe/go-chattr v1.0.1 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect diff --git a/go.sum b/go.sum index c8e0733c..48a89126 100644 --- a/go.sum +++ b/go.sum @@ -147,6 +147,8 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw github.com/franela/goblin v0.0.0-20211003143422-0a4f594942bf h1:NrF81UtW8gG2LBGkXFQFqlfNnvMt9WdB46sfdJY4oqc= github.com/franela/goblin v0.0.0-20211003143422-0a4f594942bf/go.mod h1:VzmDKDJVZI3aJmnRI9VjAn9nJ8qPPsN1fqzr9dqInIo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/g0rbe/go-chattr v1.0.1 h1:CHwYB+WKB46hkzt6Jxyvkyrz7u9njghUOFvmx2gir84= +github.com/g0rbe/go-chattr v1.0.1/go.mod h1:yQc6VPJfpDDC1g+W2t47+yYmzBNioax/GLiyJ25/IOs= github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gammazero/deque v0.2.1 h1:qSdsbG6pgp6nL7A0+K/B7s12mcCY/5l5SIUpMOl+dC0= diff --git a/server/filesystem/quotas/exfs.go b/server/filesystem/quotas/exfs.go new file mode 100644 index 00000000..f0bfdc0e --- /dev/null +++ b/server/filesystem/quotas/exfs.go @@ -0,0 +1,41 @@ +package quotas + +import ( + "os" + + "github.com/g0rbe/go-chattr" +) + +// EnableEXFSQuota enables quotas on a specified folder +func EnableEXFSQuota(serverPath string) (err error) { + serverdir, err := os.OpenFile(serverPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return + } + + err = chattr.SetAttr(serverdir, chattr.FS_PROJINHERIT_FL) + return +} + +// DisableEXFSQuota disables quotas on a specified folder +func DisableEXFSQuota(serverPath string) (err error) { + + return +} + +// SetEXFSQuota sets the quota in bytes for the specified server uuid +func SetEXFSQuota(serverUUID string, byteLimit int64) (err error) { + + return +} + +// GetEXFSQuota gets the specified quotas and usage of a specified server uuid +func GetEXFSQuota(serverUUID string) (byteLimit, bytesUsed int64, err error) { + + return +} + +// NewEXFSQuota +func NewEXFSQuota() (err error) { + return +} diff --git a/server/filesystem/quotas/syscall_quota.go b/server/filesystem/quotas/syscall_quota.go new file mode 100644 index 00000000..74020d9f --- /dev/null +++ b/server/filesystem/quotas/syscall_quota.go @@ -0,0 +1,25 @@ +package quotas + +const ( + Q_GETQUOTA = 0x0080000700 + Q_SETQUOTA = 0x0080000800 + Q_GETNEXTQUOTA = 0x00080000900 +) + +const ( + USRQUOTA = 0x0000000000 + GRPQUOTA = 0x0000000001 + PRJQUOTA = 0x0000000002 +) + +type DQBlk struct { + dqbBHardlimit uint64 + dqbBSoftlimit uint64 + dqbCurSpace uint64 + dqbIHardlimit uint64 + dqbISoftlimit uint64 + dqbCurInodes uint64 + dqbBTime uint64 + dqbITime uint64 + dqbValid uint32 +} diff --git a/server/filesystem/quotas/syscall_xattr.go b/server/filesystem/quotas/syscall_xattr.go new file mode 100644 index 00000000..5d3a72ec --- /dev/null +++ b/server/filesystem/quotas/syscall_xattr.go @@ -0,0 +1,85 @@ +package quotas + +import ( + "os" + "syscall" + "unsafe" +) + +// Pulled definitions from /usr/include/linux/fs.h + +/* + * Flags for the fsx_xflags field + */ +const ( + FS_XFLAG_REALTIME = 0x00000001 /* data in realtime volume */ + FS_XFLAG_PREALLOC = 0x00000002 /* preallocated file extents */ + FS_XFLAG_IMMUTABLE = 0x00000008 /* file cannot be modified */ + FS_XFLAG_APPEND = 0x00000010 /* all writes append */ + FS_XFLAG_SYNC = 0x00000020 /* all writes synchronous */ + FS_XFLAG_NOATIME = 0x00000040 /* do not update access time */ + FS_XFLAG_NODUMP = 0x00000080 /* do not include in backups */ + FS_XFLAG_RTINHERIT = 0x00000100 /* create with rt bit set */ + FS_XFLAG_PROJINHERIT = 0x00000200 /* create with parents projid */ + FS_XFLAG_NOSYMLINKS = 0x00000400 /* disallow symlink creation */ + FS_XFLAG_EXTSIZE = 0x00000800 /* extent size allocator hint */ + FS_XFLAG_EXTSZINHERIT = 0x00001000 /* inherit inode extent size */ + FS_XFLAG_NODEFRAG = 0x00002000 /* do not defragment */ + FS_XFLAG_FILESTREAM = 0x00004000 /* use filestream allocator */ + FS_XFLAG_DAX = 0x00008000 /* use DAX for IO */ + FS_XFLAG_COWEXTSIZE = 0x00010000 /* CoW extent size allocator hint */ + FS_XFLAG_HASATTR = 0x80000000 /* no DIFLAG for this */ +) + +/* +#define FS_IOC_GETFLAGS _IOR('f', 1, long) +#define FS_IOC_SETFLAGS _IOW('f', 2, long) +#define FS_IOC_FSGETXATTR _IOR('X', 31, struct fsxattr) +#define FS_IOC_FSSETXATTR _IOW('X', 32, struct fsxattr) +*/ + +const ( + FS_IOC_FSGETXATTR uintptr = 0x801c581f + FS_IOC_FSSETXATTR uintptr = 0x401c5820 +) + +// FSXAttr is the struct defining the structure +// for FS_IOC_FSGETXATTR and FS_IOC_FSSETXATTR +type FSXAttr struct { + XFlags uint32 + ExtSize uint32 + NextENTs uint32 + ProjectID uint32 + FSXPad byte +} + +// xAttrCtl sets the +func xAttrCtl(f *os.File, request uintptr, xattr FSXAttr) error { + _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), request, uintptr(unsafe.Pointer(&xattr))) + + if errno != 0 { + return os.NewSyscallError("ioctl", errno) + } + + return nil +} + +// getXAttr gets the extended attributes of a file +func getXAttr(f *os.File) (xattr *FSXAttr, err error) { + xattr = new(FSXAttr) + + err = xAttrCtl(f, FS_IOC_FSGETXATTR, *xattr) + + if err != nil { + return xattr, err + } + + return +} + +// setXAttr sets xattr values for the +func setXAttr(f *os.File, xattr *FSXAttr) (err error) { + err = xAttrCtl(f, FS_IOC_FSSETXATTR, *xattr) + + return +} From 40f30ffa13e26e6cdf102dceb1e1e816fab07136 Mon Sep 17 00:00:00 2001 From: "Michael (Parker) Parker" Date: Sat, 7 Feb 2026 15:25:17 -0500 Subject: [PATCH 02/15] Add filesystem based disk quotas This uses the built in quota management services to manage quotas for EXT4. May potentially work on XFS but has not been tested. See the docs for more information. https://pelican.dev/docs/guides/disk-quotas/about --- cmd/root.go | 18 +++ config/config.go | 5 + go.mod | 11 +- go.sum | 8 ++ router/router_server.go | 11 +- router/router_server_files.go | 2 +- router/router_transfer.go | 21 ++- server/activity.go | 1 - server/configuration.go | 4 + server/crash.go | 3 +- server/filesystem/quotas/exfs.go | 157 ++++++++++++++++++++-- server/filesystem/quotas/functions.go | 112 +++++++++++++++ server/filesystem/quotas/syscall_quota.go | 25 ---- server/filesystem/quotas/syscall_xattr.go | 38 +++--- server/manager.go | 15 ++- server/power.go | 24 +++- server/transfer/archive.go | 50 +++---- 17 files changed, 400 insertions(+), 105 deletions(-) create mode 100644 server/filesystem/quotas/functions.go delete mode 100644 server/filesystem/quotas/syscall_quota.go diff --git a/cmd/root.go b/cmd/root.go index c615b0b4..35c36e56 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -23,6 +23,7 @@ import ( "github.com/docker/docker/client" "github.com/gammazero/workerpool" "github.com/mitchellh/colorstring" + "github.com/pelican-dev/wings/server/filesystem/quotas" "github.com/spf13/cobra" "golang.org/x/crypto/acme" "golang.org/x/crypto/acme/autocert" @@ -191,6 +192,23 @@ func rootCmdRun(cmd *cobra.Command, _ []string) { log.WithField("server", s.ID()).Info("finished loading configuration for server") } + // if quotas are enabled ensure they are added and enabled. + if config.Get().System.Quotas.Enabled { + // check if the fs is supported + if err = quotas.IsSupportedFS(); err != nil { + log.WithField("error", err).Fatal("failed to validate quota configuration") + } + + // validate all servers are configured for quotas + for _, s := range manager.All() { + if err = quotas.AddQuota(s.Config().ID, s.Config().Uuid); err != nil { + log.WithField("error", err).Error("failed to add server to quota list") + } + } + + log.Info("quotas configured") + } + states, err := manager.ReadStates() if err != nil { log.WithField("error", err).Error("failed to retrieve locally cached server states from disk, assuming all servers in offline state") diff --git a/config/config.go b/config/config.go index cf0cd38d..78890f75 100644 --- a/config/config.go +++ b/config/config.go @@ -214,6 +214,11 @@ type SystemConfiguration struct { // disk usage is not a concern. DiskCheckInterval int64 `default:"150" yaml:"disk_check_interval"` + // Quotas define is quota management is enabled on the Data directory + Quotas struct { + Enabled bool `json:"enabled" yaml:"enabled" default:"false"` + } `json:"quotas" yaml:"quotas"` + // ActivitySendInterval is the amount of time that should ellapse between aggregated server activity // being sent to the Panel. By default this will send activity collected over the last minute. Keep // in mind that only a fixed number of activity log entries, defined by ActivitySendCount, will be sent diff --git a/go.mod b/go.mod index d26d44f7..c715c493 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/docker/go-connections v0.6.0 github.com/fatih/color v1.18.0 github.com/franela/goblin v0.0.0-20211003143422-0a4f594942bf + github.com/g0rbe/go-chattr v1.0.1 github.com/gabriel-vasile/mimetype v1.4.10 github.com/gammazero/workerpool v1.1.3 github.com/gbrlsnchs/jwt/v3 v3.0.1 @@ -35,6 +36,7 @@ require ( github.com/mattn/go-colorable v0.1.14 github.com/mholt/archives v0.1.3 github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db + github.com/parkervcp/fsquota v0.0.0-20240729140958-47d273ab9426 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pkg/sftp v1.13.9 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 @@ -63,12 +65,13 @@ require ( github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/g0rbe/go-chattr v1.0.1 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mikelolasagasti/xz v1.0.1 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect + github.com/moby/sys/mountinfo v0.7.1 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect @@ -92,7 +95,7 @@ require ( github.com/containerd/errdefs v0.3.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect @@ -135,7 +138,7 @@ require ( github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/robfig/cron/v3 v3.0.1 // indirect @@ -162,7 +165,7 @@ require ( golang.org/x/mod v0.26.0 // indirect golang.org/x/net v0.42.0 // indirect golang.org/x/text v0.28.0 // indirect - golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect + golang.org/x/time v0.0.0-20220922220347-f3bd1da661af golang.org/x/tools v0.35.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect diff --git a/go.sum b/go.sum index 48a89126..2d612116 100644 --- a/go.sum +++ b/go.sum @@ -122,6 +122,7 @@ github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbD github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM= @@ -231,6 +232,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= @@ -317,6 +320,8 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/mountinfo v0.7.1 h1:/tTvQaSJRr2FshkhXiIpux6fQ2Zvc4j7tAhMTStAG2g= +github.com/moby/sys/mountinfo v0.7.1/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae h1:O4SWKdcHVCvYqyDV+9CJA1fcDN2L11Bule0iFy3YlAI= @@ -344,6 +349,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/parkervcp/fsquota v0.0.0-20240729140958-47d273ab9426 h1:4JbdZ5mDEEz1FkH48AZsn68MGgcEwzJbYUyXeRErGk0= +github.com/parkervcp/fsquota v0.0.0-20240729140958-47d273ab9426/go.mod h1:8T5p+Jn1ZK/bz/F+QBxaMrzjpi3/lCjISQjUdVSW+DQ= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= @@ -357,6 +364,7 @@ github.com/pkg/sftp v1.13.9 h1:4NGkvGudBL7GteO3m6qnaQ4pC0Kvf0onSVc9gR3EWBw= github.com/pkg/sftp v1.13.9/go.mod h1:OBN7bVXdstkFFN/gdnHPUb5TE8eb8G1Rp9wCItqjkkA= 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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= diff --git a/router/router_server.go b/router/router_server.go index 19a1cb75..c7afd969 100644 --- a/router/router_server.go +++ b/router/router_server.go @@ -8,11 +8,11 @@ import ( "strconv" "strings" - "github.com/pelican-dev/wings/config" - "emperror.dev/errors" "github.com/apex/log" "github.com/gin-gonic/gin" + "github.com/pelican-dev/wings/config" + "github.com/pelican-dev/wings/server/filesystem/quotas" "github.com/pelican-dev/wings/router/downloader" "github.com/pelican-dev/wings/router/middleware" @@ -283,6 +283,13 @@ func deleteServer(c *gin.Context) { log.WithFields(log.Fields{"path": p, "error": err}). Warn("failed to remove server files during deletion process") } + + if config.Get().System.Quotas.Enabled { + if err = quotas.DelQuota(s.Config().Uuid); err != nil { + log.WithFields(log.Fields{"server_id": s.Config().ID, "error": err}). + Warn("failed to remove quota during deletion process") + } + } }(s) // remove hanging machine-id file for the server when removing diff --git a/router/router_server_files.go b/router/router_server_files.go index 481b7916..98d0b20b 100644 --- a/router/router_server_files.go +++ b/router/router_server_files.go @@ -240,7 +240,7 @@ func postServerDeleteFiles(c *gin.Context) { return s.Filesystem().SafeDeleteRecursively(pi) } }) - + } if err := g.Wait(); err != nil { diff --git a/router/router_transfer.go b/router/router_transfer.go index f49ef709..8ea9a5cd 100644 --- a/router/router_transfer.go +++ b/router/router_transfer.go @@ -142,14 +142,13 @@ func postTransfers(c *gin.Context) { return } - // Used to read the file and checksum from the request body. mr := multipart.NewReader(c.Request.Body, params["boundary"]) var ( - hasArchive bool - archiveChecksum string - archiveChecksumReceived string + hasArchive bool + archiveChecksum string + archiveChecksumReceived string backupChecksumsCalculated = make(map[string]string) backupChecksumsReceived = make(map[string]string) ) @@ -208,7 +207,7 @@ out: case name == "install_logs": trnsfr.Log().Debug("received install logs") - + // Create install log directory if it doesn't exist cfg := config.Get() installLogDir := filepath.Join(cfg.System.LogDirectory, "install") @@ -217,10 +216,10 @@ out: trnsfr.Log().WithError(err).Warn("failed to create install log directory, skipping") break } - + // Use the correct install log path with server UUID installLogPath := filepath.Join(installLogDir, trnsfr.Server.ID()+".log") - + // Create the install log file installLogFile, err := os.Create(installLogPath) if err != nil { @@ -228,7 +227,7 @@ out: trnsfr.Log().WithError(err).Warn("failed to create install log file, skipping") break } - + // Stream the install logs to file if _, err := io.Copy(installLogFile, p); err != nil { installLogFile.Close() @@ -236,14 +235,14 @@ out: trnsfr.Log().WithError(err).Warn("failed to stream install logs to file, skipping") break } - + if err := installLogFile.Close(); err != nil { // Don't fail transfer for install logs, just log and continue trnsfr.Log().WithError(err).Warn("failed to close install log file") } - + trnsfr.Log().WithField("path", installLogPath).Debug("install logs saved successfully") - + case strings.HasPrefix(name, "backup_"): backupName := strings.TrimPrefix(name, "backup_") trnsfr.Log().WithField("backup", backupName).Debug("received backup file") diff --git a/server/activity.go b/server/activity.go index 584e40c4..271d022f 100644 --- a/server/activity.go +++ b/server/activity.go @@ -21,7 +21,6 @@ const ( ActivitySftpDelete = models.Event("server:sftp.delete") ActivityFileUploaded = models.Event("server:file.uploaded") ActivityServerCrashed = models.Event("server:crashed") - ) // RequestActivity is a wrapper around a LoggedEvent that is able to track additional request diff --git a/server/configuration.go b/server/configuration.go index 15d74a56..753584da 100644 --- a/server/configuration.go +++ b/server/configuration.go @@ -46,6 +46,10 @@ type ConfigurationMeta struct { type Configuration struct { mu sync.RWMutex + // ID is the database id from the panel that is guaranteed to be unique + // this is being used for quotas + ID int `json:"id"` + // The unique identifier for the server that should be used when referencing // it against the Panel API (and internally). This will be used when naming // docker containers as well as in log output. diff --git a/server/crash.go b/server/crash.go index 3be6587e..2e7b3ee0 100644 --- a/server/crash.go +++ b/server/crash.go @@ -12,7 +12,6 @@ import ( "github.com/pelican-dev/wings/config" "github.com/pelican-dev/wings/environment" "github.com/pelican-dev/wings/internal/models" - ) type CrashHandler struct { @@ -100,7 +99,7 @@ func (s *Server) handleServerCrash() error { "oomkilled": oomKilled, "logs": logs, }) - + s.crasher.SetLastCrash(time.Now()) return errors.Wrap(s.HandlePowerAction(PowerActionStart), "failed to start server after crash detection") diff --git a/server/filesystem/quotas/exfs.go b/server/filesystem/quotas/exfs.go index f0bfdc0e..24125b86 100644 --- a/server/filesystem/quotas/exfs.go +++ b/server/filesystem/quotas/exfs.go @@ -1,41 +1,172 @@ package quotas import ( + "fmt" + "html/template" "os" + "strings" - "github.com/g0rbe/go-chattr" + "github.com/parkervcp/fsquota" + "github.com/pelican-dev/wings/config" ) -// EnableEXFSQuota enables quotas on a specified folder -func EnableEXFSQuota(serverPath string) (err error) { - serverdir, err := os.OpenFile(serverPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) +var exfsProjects []exfsProject + +type exfsProject struct { + ID int + Name string + BasePath string +} + +const ( + projidTemplate = `{{ range . }}{{ .UUID }}:{{ .ID }} +{{ end }}` + projectsTemplate = `{{ range . }}{{ .ID }}:{{ .BasePath }}/{{ .UUID }} +{{ end }}` + + projidFile = `/etc/projid` + projectFile = `/etc/projects` +) + +// setQuota sets the quota in bytes for the specified server uuid +func (q exfsProject) setQuota(byteLimit uint64) (err error) { + serverProject, err := fsquota.LookupProject(q.Name) + if err != nil { + return + } + + serverDirPath := fmt.Sprintf("%s/%s", config.Get().System.Data, q.Name) + projInfo, err := fsquota.GetProjectInfo(serverDirPath, serverProject) if err != nil { return } - err = chattr.SetAttr(serverdir, chattr.FS_PROJINHERIT_FL) + projInfo.Limits.Bytes.SetHard(byteLimit) + + if _, err = fsquota.SetProjectQuota(serverDirPath, serverProject, projInfo.Limits); err != nil { + return + } return } -// DisableEXFSQuota disables quotas on a specified folder -func DisableEXFSQuota(serverPath string) (err error) { +// getQuota gets the specified quotas and usage of a specified server uuid +func (q exfsProject) getQuota() (bytesUsed int64, err error) { + serverProject, err := fsquota.LookupProject(q.Name) + if err != nil { + return + } + + projInfo, err := fsquota.GetProjectInfo(q.BasePath, serverProject) + if err != nil { + return + } + + // converts the uint64 to int64. + // This should only be an issue in the terms of exabytes... + return int64(projInfo.BytesUsed), nil +} + +// enableEXFSQuota enables quotas on a specified directory +func (q exfsProject) enableEXFSQuota() (err error) { + serverDir, err := os.OpenFile(fmt.Sprintf("%s/%s", q.BasePath, q.Name), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) + if err != nil { + return + } + + defer serverDir.Close() + + folderXattr, err := getXAttr(serverDir) + if err != nil { + return + } + + // ensure project inherit flag is set + if (folderXattr.XFlags & FS_XFLAG_PROJINHERIT) != 0 { + if err = setXAttr(serverDir, fsXAttr{XFlags: FS_XFLAG_PROJINHERIT}); err != nil { + return + } + } + + // ensure correct project id is set + if folderXattr.ProjectID != uint32(q.ID) { + if err = setXAttr(serverDir, fsXAttr{ProjectID: uint32(q.ID)}); err != nil { + return + } + } return } -// SetEXFSQuota sets the quota in bytes for the specified server uuid -func SetEXFSQuota(serverUUID string, byteLimit int64) (err error) { +func (q exfsProject) addProject() (err error) { + basePath := config.Get().System.Data + if strings.HasSuffix(basePath, "/") { + basePath = strings.TrimSuffix(config.Get().System.Data, "/") + } + + q.BasePath = basePath + exfsProjects = append(exfsProjects, q) + + if err = writeEXFSProjects(); err != nil { + return + } + + if err = q.enableEXFSQuota(); err != nil { + return + } return } -// GetEXFSQuota gets the specified quotas and usage of a specified server uuid -func GetEXFSQuota(serverUUID string) (byteLimit, bytesUsed int64, err error) { +// removeProject drops a specified project from the +func (q exfsProject) removeProject() (err error) { + for pos, project := range exfsProjects { + if project.Name == q.Name { + exfsProjects = append(exfsProjects[:pos], exfsProjects[pos+1:]...) + } + } + err = writeEXFSProjects() return } -// NewEXFSQuota -func NewEXFSQuota() (err error) { +func writeEXFSProjects() (err error) { + // write out projid file + idtmpl, err := template.New("projid").Parse(projidTemplate) + if err != nil { + return + } + + if err = writeTemplate(idtmpl, projidFile, exfsProjects); err != nil { + return + } + + projtmpl, err := template.New("projects").Parse(projectsTemplate) + if err != nil { + return + } + + if err = writeTemplate(projtmpl, projectFile, exfsProjects); err != nil { + return + } + return } + +func writeTemplate(t *template.Template, file string, data interface{}) (err error) { + f, err := os.Create(file) + if err != nil { + return err + } + + err = t.Execute(f, data) + if err != nil { + return err + } + + err = f.Close() + if err != nil { + return err + } + + return nil +} diff --git a/server/filesystem/quotas/functions.go b/server/filesystem/quotas/functions.go new file mode 100644 index 00000000..0badc1ae --- /dev/null +++ b/server/filesystem/quotas/functions.go @@ -0,0 +1,112 @@ +package quotas + +import ( + "syscall" + + "emperror.dev/errors" + "github.com/parkervcp/fsquota" + "github.com/pelican-dev/wings/config" +) + +const ( + FSBTRFS = 2435016766 + FSEXT4 = 61267 + FSXFS = 1481003842 + FSZFS = 801189825 +) + +var fstype string + +func getFSType(mount string) (fsType uint, err error) { + var stat syscall.Statfs_t + + if mount == "" { + return fsType, errors.New("must specify path to check the filesystem type") + } + + err = syscall.Statfs(mount, &stat) + if err != nil { + return fsType, err + } + + switch stat.Type { + case FSBTRFS: + return FSBTRFS, nil + case FSEXT4: + return FSEXT4, nil + case FSXFS: + return FSXFS, nil + case FSZFS: + return FSZFS, nil + default: + return fsType, errors.New("unknown filesystem type") + } +} + +// IsSupportedFS checks if the filesystem for the data files is supported. +// currently only EXT4 and XFS are supported +func IsSupportedFS() (err error) { + checked, err := getFSType(config.Get().System.Data) + if err != nil { + return err + } + + switch checked { + case FSEXT4 | FSXFS: + // technically tested on EXT4 and will need to be validated for XFS + supported, err := fsquota.ProjectQuotasSupported(config.Get().System.Data) + if err != nil { + return + } + if !supported { + return errors.New("project quotas not enabled") + } + fstype = "exfs" + return + case FSBTRFS: + fstype = "btrfs" + return errors.New("btrfs is not supported on this filesystem") + case FSZFS: + fstype = "zfs" + return errors.New("zfs is not supported on this filesystem") + default: + return errors.New("unknown filesystem type") + } +} + +// AddQuota adds a server to the configured quotas +func AddQuota(serverID int, serverUUID string) (err error) { + switch fstype { + case "exfs": + err = exfsProject{ID: serverID, Name: serverUUID}.addProject() + } + + return +} + +// DelQuota removes a server from the configured quotas +func DelQuota(serverUUID string) (err error) { + switch fstype { + case "exfs": + err = exfsProject{Name: serverUUID}.removeProject() + } + return +} + +// SetQuota configures quotas for a specified server +func SetQuota(limit int64, serverUUID string) (err error) { + switch fstype { + case "exfs": + err = exfsProject{Name: serverUUID}.setQuota(uint64(limit)) + } + return +} + +// GetQuota gets the data usage for a specified server +func GetQuota(serverUUID string) (used int64, err error) { + switch fstype { + case "exfs": + used, err = exfsProject{Name: serverUUID}.getQuota() + } + return +} diff --git a/server/filesystem/quotas/syscall_quota.go b/server/filesystem/quotas/syscall_quota.go deleted file mode 100644 index 74020d9f..00000000 --- a/server/filesystem/quotas/syscall_quota.go +++ /dev/null @@ -1,25 +0,0 @@ -package quotas - -const ( - Q_GETQUOTA = 0x0080000700 - Q_SETQUOTA = 0x0080000800 - Q_GETNEXTQUOTA = 0x00080000900 -) - -const ( - USRQUOTA = 0x0000000000 - GRPQUOTA = 0x0000000001 - PRJQUOTA = 0x0000000002 -) - -type DQBlk struct { - dqbBHardlimit uint64 - dqbBSoftlimit uint64 - dqbCurSpace uint64 - dqbIHardlimit uint64 - dqbISoftlimit uint64 - dqbCurInodes uint64 - dqbBTime uint64 - dqbITime uint64 - dqbValid uint32 -} diff --git a/server/filesystem/quotas/syscall_xattr.go b/server/filesystem/quotas/syscall_xattr.go index 5d3a72ec..f1ff6167 100644 --- a/server/filesystem/quotas/syscall_xattr.go +++ b/server/filesystem/quotas/syscall_xattr.go @@ -39,13 +39,13 @@ const ( */ const ( - FS_IOC_FSGETXATTR uintptr = 0x801c581f - FS_IOC_FSSETXATTR uintptr = 0x401c5820 + FS_IOC_FSGETXATTR uintptr = 0x801c581f // https://docs.rs/linux-raw-sys/latest/linux_raw_sys/ioctl/constant.FS_IOC_FSGETXATTR.html + FS_IOC_FSSETXATTR uintptr = 0x401c5820 // https://docs.rs/linux-raw-sys/latest/linux_raw_sys/ioctl/constant.FS_IOC_FSSETXATTR.html ) -// FSXAttr is the struct defining the structure +// fsXAttr is the struct defining the structure // for FS_IOC_FSGETXATTR and FS_IOC_FSSETXATTR -type FSXAttr struct { +type fsXAttr struct { XFlags uint32 ExtSize uint32 NextENTs uint32 @@ -54,32 +54,38 @@ type FSXAttr struct { } // xAttrCtl sets the -func xAttrCtl(f *os.File, request uintptr, xattr FSXAttr) error { - _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), request, uintptr(unsafe.Pointer(&xattr))) +func xAttrCtl(f *os.File, request uintptr, xattr *fsXAttr) (err error) { + attreq := uintptr(unsafe.Pointer(xattr)) + + _, _, errno := syscall.RawSyscall(syscall.SYS_IOCTL, f.Fd(), request, attreq) if errno != 0 { return os.NewSyscallError("ioctl", errno) } - return nil + return } // getXAttr gets the extended attributes of a file -func getXAttr(f *os.File) (xattr *FSXAttr, err error) { - xattr = new(FSXAttr) - - err = xAttrCtl(f, FS_IOC_FSGETXATTR, *xattr) - - if err != nil { - return xattr, err +func getXAttr(f *os.File) (attr fsXAttr, err error) { + if err = xAttrCtl(f, FS_IOC_FSGETXATTR, &attr); err != nil { + return } return } // setXAttr sets xattr values for the -func setXAttr(f *os.File, xattr *FSXAttr) (err error) { - err = xAttrCtl(f, FS_IOC_FSSETXATTR, *xattr) +func setXAttr(serverDir *os.File, fsXAttr fsXAttr) (err error) { + xAttr, err := getXAttr(serverDir) + if err != nil { + return err + } + + // bitwise add for uint32 X Attributes + xAttr.XFlags |= fsXAttr.XFlags + + err = xAttrCtl(serverDir, FS_IOC_FSSETXATTR, &fsXAttr) return } diff --git a/server/manager.go b/server/manager.go index 55dd645e..a9363478 100644 --- a/server/manager.go +++ b/server/manager.go @@ -14,12 +14,12 @@ import ( "github.com/apex/log" "github.com/gammazero/workerpool" "github.com/goccy/go-json" - "github.com/pelican-dev/wings/config" "github.com/pelican-dev/wings/environment" "github.com/pelican-dev/wings/environment/docker" "github.com/pelican-dev/wings/remote" "github.com/pelican-dev/wings/server/filesystem" + "github.com/pelican-dev/wings/server/filesystem/quotas" ) type Manager struct { @@ -192,7 +192,7 @@ func (m *Manager) InitServer(data remote.ServerConfigurationResponse) (*Server, // Setup the base server configuration data which will be used for all of the // remaining functionality in this call. - if err := s.SyncWithConfiguration(data); err != nil { + if err = s.SyncWithConfiguration(data); err != nil { return nil, errors.WithStackIf(err) } @@ -201,6 +201,17 @@ func (m *Manager) InitServer(data remote.ServerConfigurationResponse) (*Server, return nil, errors.WithStackIf(err) } + // if quotas are enabled ensure quotas are configured + if config.Get().System.Quotas.Enabled { + if err = quotas.AddQuota(s.cfg.ID, s.Config().Uuid); err != nil { + return nil, errors.WithStackIf(err) + } + + if err = quotas.SetQuota(s.Environment.Config().Limits().DiskSpace, s.Config().Uuid); err != nil { + return nil, errors.WithStackIf(err) + } + } + // Right now we only support a Docker based environment, so I'm going to hard code // this logic in. When we're ready to support other environment we'll need to make // some modifications here, obviously. diff --git a/server/power.go b/server/power.go index 622fd06c..a33de61d 100644 --- a/server/power.go +++ b/server/power.go @@ -6,10 +6,11 @@ import ( "time" "emperror.dev/errors" + "github.com/apex/log" "github.com/google/uuid" - "github.com/pelican-dev/wings/config" "github.com/pelican-dev/wings/environment" + "github.com/pelican-dev/wings/server/filesystem/quotas" ) type PowerAction string @@ -190,8 +191,25 @@ func (s *Server) onBeforeStart() error { s.Filesystem().HasSpaceAvailable(true) } else { s.PublishConsoleOutputFromDaemon("Checking server disk space usage, this could take a few seconds...") - if err := s.Filesystem().HasSpaceErr(false); err != nil { - return err + if config.Get().System.Quotas.Enabled { + // if quotas are enabled used the quota system to check usage + // ensure quotas are set before checking space used + if err := quotas.SetQuota(s.Environment.Config().Limits().DiskSpace, s.Config().Uuid); err != nil { + log.WithField("error", err).Error("failed to set quota for server") + } + + used, err := quotas.GetQuota(s.Config().Uuid) + if err != nil { + log.WithField("error", err).Error("failed to get quota for server") + } + + if used >= s.Environment.Config().Limits().DiskSpace { + return errors.New("quota for server is too large") + } + } else { + if err := s.Filesystem().HasSpaceErr(false); err != nil { + return err + } } } diff --git a/server/transfer/archive.go b/server/transfer/archive.go index 8f6cfc59..366750d4 100644 --- a/server/transfer/archive.go +++ b/server/transfer/archive.go @@ -37,10 +37,10 @@ func (t *Transfer) Archive() (*Archive, error) { func (a *Archive) StreamBackups(ctx context.Context, mp *multipart.Writer) error { // In theory this can't happen as this function is only called if there is at least 1 backup but just to be sure if len(a.transfer.BackupUUIDs) == 0 { - a.transfer.Log().Debug("no backups specified for transfer") - return nil - } - + a.transfer.Log().Debug("no backups specified for transfer") + return nil + } + cfg := config.Get() backupPath := filepath.Join(cfg.System.BackupDirectory, a.transfer.Server.ID()) @@ -55,27 +55,27 @@ func (a *Archive) StreamBackups(ctx context.Context, mp *multipart.Writer) error return err } - // Create a set of backup UUIDs for quick lookup - backupSet := make(map[string]bool) - for _, uuid := range a.transfer.BackupUUIDs { - backupSet[uuid+".tar.gz"] = true // Backup files are stored as UUID.tar.gz - } - - var backupsToTransfer []os.DirEntry - for _, entry := range entries { - if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".tar.gz") { - if backupSet[entry.Name()] { - backupsToTransfer = append(backupsToTransfer, entry) - } - } - } - - totalBackups := len(backupsToTransfer) - if totalBackups == 0 { - a.transfer.Log().Debug("no matching backup files found") - return nil - } - + // Create a set of backup UUIDs for quick lookup + backupSet := make(map[string]bool) + for _, uuid := range a.transfer.BackupUUIDs { + backupSet[uuid+".tar.gz"] = true // Backup files are stored as UUID.tar.gz + } + + var backupsToTransfer []os.DirEntry + for _, entry := range entries { + if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".tar.gz") { + if backupSet[entry.Name()] { + backupsToTransfer = append(backupsToTransfer, entry) + } + } + } + + totalBackups := len(backupsToTransfer) + if totalBackups == 0 { + a.transfer.Log().Debug("no matching backup files found") + return nil + } + a.transfer.Log().Infof("Starting transfer of %d backup files", totalBackups) a.transfer.SendMessage(fmt.Sprintf("Starting transfer of %d backup files", totalBackups)) From 7b9fc35af5d6df424b065c51984503fd8de3bd5b Mon Sep 17 00:00:00 2001 From: "Michael (Parker) Parker" Date: Sun, 8 Feb 2026 13:05:02 -0500 Subject: [PATCH 03/15] fix issue with returns builds were failing due to un-used err --- server/filesystem/quotas/functions.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/filesystem/quotas/functions.go b/server/filesystem/quotas/functions.go index 0badc1ae..563da794 100644 --- a/server/filesystem/quotas/functions.go +++ b/server/filesystem/quotas/functions.go @@ -56,13 +56,14 @@ func IsSupportedFS() (err error) { // technically tested on EXT4 and will need to be validated for XFS supported, err := fsquota.ProjectQuotasSupported(config.Get().System.Data) if err != nil { - return + return err } if !supported { return errors.New("project quotas not enabled") } + fstype = "exfs" - return + return err case FSBTRFS: fstype = "btrfs" return errors.New("btrfs is not supported on this filesystem") From bd5cf703b5b17024f3664b95ce725ff16ad5b62c Mon Sep 17 00:00:00 2001 From: "Michael (Parker) Parker" Date: Wed, 14 Jan 2026 08:38:26 -0500 Subject: [PATCH 04/15] Start disk quota package --- go.mod | 1 + go.sum | 2 + server/filesystem/quotas/exfs.go | 41 +++++++++++ server/filesystem/quotas/syscall_quota.go | 25 +++++++ server/filesystem/quotas/syscall_xattr.go | 85 +++++++++++++++++++++++ 5 files changed, 154 insertions(+) create mode 100644 server/filesystem/quotas/exfs.go create mode 100644 server/filesystem/quotas/syscall_quota.go create mode 100644 server/filesystem/quotas/syscall_xattr.go diff --git a/go.mod b/go.mod index cc9f0fee..80a80b10 100644 --- a/go.mod +++ b/go.mod @@ -64,6 +64,7 @@ require ( github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/g0rbe/go-chattr v1.0.1 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect diff --git a/go.sum b/go.sum index a560d283..7948c609 100644 --- a/go.sum +++ b/go.sum @@ -145,6 +145,8 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw github.com/franela/goblin v0.0.0-20211003143422-0a4f594942bf h1:NrF81UtW8gG2LBGkXFQFqlfNnvMt9WdB46sfdJY4oqc= github.com/franela/goblin v0.0.0-20211003143422-0a4f594942bf/go.mod h1:VzmDKDJVZI3aJmnRI9VjAn9nJ8qPPsN1fqzr9dqInIo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/g0rbe/go-chattr v1.0.1 h1:CHwYB+WKB46hkzt6Jxyvkyrz7u9njghUOFvmx2gir84= +github.com/g0rbe/go-chattr v1.0.1/go.mod h1:yQc6VPJfpDDC1g+W2t47+yYmzBNioax/GLiyJ25/IOs= github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gammazero/deque v0.2.1 h1:qSdsbG6pgp6nL7A0+K/B7s12mcCY/5l5SIUpMOl+dC0= diff --git a/server/filesystem/quotas/exfs.go b/server/filesystem/quotas/exfs.go new file mode 100644 index 00000000..f0bfdc0e --- /dev/null +++ b/server/filesystem/quotas/exfs.go @@ -0,0 +1,41 @@ +package quotas + +import ( + "os" + + "github.com/g0rbe/go-chattr" +) + +// EnableEXFSQuota enables quotas on a specified folder +func EnableEXFSQuota(serverPath string) (err error) { + serverdir, err := os.OpenFile(serverPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return + } + + err = chattr.SetAttr(serverdir, chattr.FS_PROJINHERIT_FL) + return +} + +// DisableEXFSQuota disables quotas on a specified folder +func DisableEXFSQuota(serverPath string) (err error) { + + return +} + +// SetEXFSQuota sets the quota in bytes for the specified server uuid +func SetEXFSQuota(serverUUID string, byteLimit int64) (err error) { + + return +} + +// GetEXFSQuota gets the specified quotas and usage of a specified server uuid +func GetEXFSQuota(serverUUID string) (byteLimit, bytesUsed int64, err error) { + + return +} + +// NewEXFSQuota +func NewEXFSQuota() (err error) { + return +} diff --git a/server/filesystem/quotas/syscall_quota.go b/server/filesystem/quotas/syscall_quota.go new file mode 100644 index 00000000..74020d9f --- /dev/null +++ b/server/filesystem/quotas/syscall_quota.go @@ -0,0 +1,25 @@ +package quotas + +const ( + Q_GETQUOTA = 0x0080000700 + Q_SETQUOTA = 0x0080000800 + Q_GETNEXTQUOTA = 0x00080000900 +) + +const ( + USRQUOTA = 0x0000000000 + GRPQUOTA = 0x0000000001 + PRJQUOTA = 0x0000000002 +) + +type DQBlk struct { + dqbBHardlimit uint64 + dqbBSoftlimit uint64 + dqbCurSpace uint64 + dqbIHardlimit uint64 + dqbISoftlimit uint64 + dqbCurInodes uint64 + dqbBTime uint64 + dqbITime uint64 + dqbValid uint32 +} diff --git a/server/filesystem/quotas/syscall_xattr.go b/server/filesystem/quotas/syscall_xattr.go new file mode 100644 index 00000000..5d3a72ec --- /dev/null +++ b/server/filesystem/quotas/syscall_xattr.go @@ -0,0 +1,85 @@ +package quotas + +import ( + "os" + "syscall" + "unsafe" +) + +// Pulled definitions from /usr/include/linux/fs.h + +/* + * Flags for the fsx_xflags field + */ +const ( + FS_XFLAG_REALTIME = 0x00000001 /* data in realtime volume */ + FS_XFLAG_PREALLOC = 0x00000002 /* preallocated file extents */ + FS_XFLAG_IMMUTABLE = 0x00000008 /* file cannot be modified */ + FS_XFLAG_APPEND = 0x00000010 /* all writes append */ + FS_XFLAG_SYNC = 0x00000020 /* all writes synchronous */ + FS_XFLAG_NOATIME = 0x00000040 /* do not update access time */ + FS_XFLAG_NODUMP = 0x00000080 /* do not include in backups */ + FS_XFLAG_RTINHERIT = 0x00000100 /* create with rt bit set */ + FS_XFLAG_PROJINHERIT = 0x00000200 /* create with parents projid */ + FS_XFLAG_NOSYMLINKS = 0x00000400 /* disallow symlink creation */ + FS_XFLAG_EXTSIZE = 0x00000800 /* extent size allocator hint */ + FS_XFLAG_EXTSZINHERIT = 0x00001000 /* inherit inode extent size */ + FS_XFLAG_NODEFRAG = 0x00002000 /* do not defragment */ + FS_XFLAG_FILESTREAM = 0x00004000 /* use filestream allocator */ + FS_XFLAG_DAX = 0x00008000 /* use DAX for IO */ + FS_XFLAG_COWEXTSIZE = 0x00010000 /* CoW extent size allocator hint */ + FS_XFLAG_HASATTR = 0x80000000 /* no DIFLAG for this */ +) + +/* +#define FS_IOC_GETFLAGS _IOR('f', 1, long) +#define FS_IOC_SETFLAGS _IOW('f', 2, long) +#define FS_IOC_FSGETXATTR _IOR('X', 31, struct fsxattr) +#define FS_IOC_FSSETXATTR _IOW('X', 32, struct fsxattr) +*/ + +const ( + FS_IOC_FSGETXATTR uintptr = 0x801c581f + FS_IOC_FSSETXATTR uintptr = 0x401c5820 +) + +// FSXAttr is the struct defining the structure +// for FS_IOC_FSGETXATTR and FS_IOC_FSSETXATTR +type FSXAttr struct { + XFlags uint32 + ExtSize uint32 + NextENTs uint32 + ProjectID uint32 + FSXPad byte +} + +// xAttrCtl sets the +func xAttrCtl(f *os.File, request uintptr, xattr FSXAttr) error { + _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), request, uintptr(unsafe.Pointer(&xattr))) + + if errno != 0 { + return os.NewSyscallError("ioctl", errno) + } + + return nil +} + +// getXAttr gets the extended attributes of a file +func getXAttr(f *os.File) (xattr *FSXAttr, err error) { + xattr = new(FSXAttr) + + err = xAttrCtl(f, FS_IOC_FSGETXATTR, *xattr) + + if err != nil { + return xattr, err + } + + return +} + +// setXAttr sets xattr values for the +func setXAttr(f *os.File, xattr *FSXAttr) (err error) { + err = xAttrCtl(f, FS_IOC_FSSETXATTR, *xattr) + + return +} From 0a6149cb6b112c8375321ebe21a7d8272afe05e4 Mon Sep 17 00:00:00 2001 From: "Michael (Parker) Parker" Date: Sat, 7 Feb 2026 15:25:17 -0500 Subject: [PATCH 05/15] Add filesystem based disk quotas This uses the built in quota management services to manage quotas for EXT4. May potentially work on XFS but has not been tested. See the docs for more information. https://pelican.dev/docs/guides/disk-quotas/about --- cmd/root.go | 18 +++ config/config.go | 5 + go.mod | 11 +- go.sum | 8 ++ router/router_server.go | 11 +- router/router_server_files.go | 2 +- router/router_transfer.go | 21 ++- server/activity.go | 1 - server/configuration.go | 4 + server/crash.go | 3 +- server/filesystem/quotas/exfs.go | 157 ++++++++++++++++++++-- server/filesystem/quotas/functions.go | 112 +++++++++++++++ server/filesystem/quotas/syscall_quota.go | 25 ---- server/filesystem/quotas/syscall_xattr.go | 38 +++--- server/manager.go | 15 ++- server/power.go | 24 +++- server/transfer/archive.go | 50 +++---- 17 files changed, 400 insertions(+), 105 deletions(-) create mode 100644 server/filesystem/quotas/functions.go delete mode 100644 server/filesystem/quotas/syscall_quota.go diff --git a/cmd/root.go b/cmd/root.go index c615b0b4..35c36e56 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -23,6 +23,7 @@ import ( "github.com/docker/docker/client" "github.com/gammazero/workerpool" "github.com/mitchellh/colorstring" + "github.com/pelican-dev/wings/server/filesystem/quotas" "github.com/spf13/cobra" "golang.org/x/crypto/acme" "golang.org/x/crypto/acme/autocert" @@ -191,6 +192,23 @@ func rootCmdRun(cmd *cobra.Command, _ []string) { log.WithField("server", s.ID()).Info("finished loading configuration for server") } + // if quotas are enabled ensure they are added and enabled. + if config.Get().System.Quotas.Enabled { + // check if the fs is supported + if err = quotas.IsSupportedFS(); err != nil { + log.WithField("error", err).Fatal("failed to validate quota configuration") + } + + // validate all servers are configured for quotas + for _, s := range manager.All() { + if err = quotas.AddQuota(s.Config().ID, s.Config().Uuid); err != nil { + log.WithField("error", err).Error("failed to add server to quota list") + } + } + + log.Info("quotas configured") + } + states, err := manager.ReadStates() if err != nil { log.WithField("error", err).Error("failed to retrieve locally cached server states from disk, assuming all servers in offline state") diff --git a/config/config.go b/config/config.go index cf0cd38d..78890f75 100644 --- a/config/config.go +++ b/config/config.go @@ -214,6 +214,11 @@ type SystemConfiguration struct { // disk usage is not a concern. DiskCheckInterval int64 `default:"150" yaml:"disk_check_interval"` + // Quotas define is quota management is enabled on the Data directory + Quotas struct { + Enabled bool `json:"enabled" yaml:"enabled" default:"false"` + } `json:"quotas" yaml:"quotas"` + // ActivitySendInterval is the amount of time that should ellapse between aggregated server activity // being sent to the Panel. By default this will send activity collected over the last minute. Keep // in mind that only a fixed number of activity log entries, defined by ActivitySendCount, will be sent diff --git a/go.mod b/go.mod index 80a80b10..434951d9 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/docker/go-connections v0.6.0 github.com/fatih/color v1.18.0 github.com/franela/goblin v0.0.0-20211003143422-0a4f594942bf + github.com/g0rbe/go-chattr v1.0.1 github.com/gabriel-vasile/mimetype v1.4.10 github.com/gammazero/workerpool v1.1.3 github.com/gbrlsnchs/jwt/v3 v3.0.1 @@ -34,6 +35,7 @@ require ( github.com/mattn/go-colorable v0.1.14 github.com/mholt/archives v0.1.3 github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db + github.com/parkervcp/fsquota v0.0.0-20240729140958-47d273ab9426 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pkg/sftp v1.13.9 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 @@ -64,12 +66,13 @@ require ( github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/g0rbe/go-chattr v1.0.1 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mikelolasagasti/xz v1.0.1 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect + github.com/moby/sys/mountinfo v0.7.1 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect @@ -95,7 +98,7 @@ require ( github.com/containerd/errdefs v0.3.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect @@ -138,7 +141,7 @@ require ( github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/robfig/cron/v3 v3.0.1 // indirect @@ -165,7 +168,7 @@ require ( golang.org/x/mod v0.26.0 // indirect golang.org/x/net v0.42.0 // indirect golang.org/x/text v0.28.0 // indirect - golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect + golang.org/x/time v0.0.0-20220922220347-f3bd1da661af golang.org/x/tools v0.35.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect diff --git a/go.sum b/go.sum index 7948c609..1cc9f82f 100644 --- a/go.sum +++ b/go.sum @@ -120,6 +120,7 @@ github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbD github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM= @@ -229,6 +230,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= @@ -315,6 +318,8 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/mountinfo v0.7.1 h1:/tTvQaSJRr2FshkhXiIpux6fQ2Zvc4j7tAhMTStAG2g= +github.com/moby/sys/mountinfo v0.7.1/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae h1:O4SWKdcHVCvYqyDV+9CJA1fcDN2L11Bule0iFy3YlAI= @@ -342,6 +347,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/parkervcp/fsquota v0.0.0-20240729140958-47d273ab9426 h1:4JbdZ5mDEEz1FkH48AZsn68MGgcEwzJbYUyXeRErGk0= +github.com/parkervcp/fsquota v0.0.0-20240729140958-47d273ab9426/go.mod h1:8T5p+Jn1ZK/bz/F+QBxaMrzjpi3/lCjISQjUdVSW+DQ= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= @@ -355,6 +362,7 @@ github.com/pkg/sftp v1.13.9 h1:4NGkvGudBL7GteO3m6qnaQ4pC0Kvf0onSVc9gR3EWBw= github.com/pkg/sftp v1.13.9/go.mod h1:OBN7bVXdstkFFN/gdnHPUb5TE8eb8G1Rp9wCItqjkkA= 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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= diff --git a/router/router_server.go b/router/router_server.go index 19a1cb75..c7afd969 100644 --- a/router/router_server.go +++ b/router/router_server.go @@ -8,11 +8,11 @@ import ( "strconv" "strings" - "github.com/pelican-dev/wings/config" - "emperror.dev/errors" "github.com/apex/log" "github.com/gin-gonic/gin" + "github.com/pelican-dev/wings/config" + "github.com/pelican-dev/wings/server/filesystem/quotas" "github.com/pelican-dev/wings/router/downloader" "github.com/pelican-dev/wings/router/middleware" @@ -283,6 +283,13 @@ func deleteServer(c *gin.Context) { log.WithFields(log.Fields{"path": p, "error": err}). Warn("failed to remove server files during deletion process") } + + if config.Get().System.Quotas.Enabled { + if err = quotas.DelQuota(s.Config().Uuid); err != nil { + log.WithFields(log.Fields{"server_id": s.Config().ID, "error": err}). + Warn("failed to remove quota during deletion process") + } + } }(s) // remove hanging machine-id file for the server when removing diff --git a/router/router_server_files.go b/router/router_server_files.go index 481b7916..98d0b20b 100644 --- a/router/router_server_files.go +++ b/router/router_server_files.go @@ -240,7 +240,7 @@ func postServerDeleteFiles(c *gin.Context) { return s.Filesystem().SafeDeleteRecursively(pi) } }) - + } if err := g.Wait(); err != nil { diff --git a/router/router_transfer.go b/router/router_transfer.go index f49ef709..8ea9a5cd 100644 --- a/router/router_transfer.go +++ b/router/router_transfer.go @@ -142,14 +142,13 @@ func postTransfers(c *gin.Context) { return } - // Used to read the file and checksum from the request body. mr := multipart.NewReader(c.Request.Body, params["boundary"]) var ( - hasArchive bool - archiveChecksum string - archiveChecksumReceived string + hasArchive bool + archiveChecksum string + archiveChecksumReceived string backupChecksumsCalculated = make(map[string]string) backupChecksumsReceived = make(map[string]string) ) @@ -208,7 +207,7 @@ out: case name == "install_logs": trnsfr.Log().Debug("received install logs") - + // Create install log directory if it doesn't exist cfg := config.Get() installLogDir := filepath.Join(cfg.System.LogDirectory, "install") @@ -217,10 +216,10 @@ out: trnsfr.Log().WithError(err).Warn("failed to create install log directory, skipping") break } - + // Use the correct install log path with server UUID installLogPath := filepath.Join(installLogDir, trnsfr.Server.ID()+".log") - + // Create the install log file installLogFile, err := os.Create(installLogPath) if err != nil { @@ -228,7 +227,7 @@ out: trnsfr.Log().WithError(err).Warn("failed to create install log file, skipping") break } - + // Stream the install logs to file if _, err := io.Copy(installLogFile, p); err != nil { installLogFile.Close() @@ -236,14 +235,14 @@ out: trnsfr.Log().WithError(err).Warn("failed to stream install logs to file, skipping") break } - + if err := installLogFile.Close(); err != nil { // Don't fail transfer for install logs, just log and continue trnsfr.Log().WithError(err).Warn("failed to close install log file") } - + trnsfr.Log().WithField("path", installLogPath).Debug("install logs saved successfully") - + case strings.HasPrefix(name, "backup_"): backupName := strings.TrimPrefix(name, "backup_") trnsfr.Log().WithField("backup", backupName).Debug("received backup file") diff --git a/server/activity.go b/server/activity.go index 584e40c4..271d022f 100644 --- a/server/activity.go +++ b/server/activity.go @@ -21,7 +21,6 @@ const ( ActivitySftpDelete = models.Event("server:sftp.delete") ActivityFileUploaded = models.Event("server:file.uploaded") ActivityServerCrashed = models.Event("server:crashed") - ) // RequestActivity is a wrapper around a LoggedEvent that is able to track additional request diff --git a/server/configuration.go b/server/configuration.go index 15d74a56..753584da 100644 --- a/server/configuration.go +++ b/server/configuration.go @@ -46,6 +46,10 @@ type ConfigurationMeta struct { type Configuration struct { mu sync.RWMutex + // ID is the database id from the panel that is guaranteed to be unique + // this is being used for quotas + ID int `json:"id"` + // The unique identifier for the server that should be used when referencing // it against the Panel API (and internally). This will be used when naming // docker containers as well as in log output. diff --git a/server/crash.go b/server/crash.go index 3be6587e..2e7b3ee0 100644 --- a/server/crash.go +++ b/server/crash.go @@ -12,7 +12,6 @@ import ( "github.com/pelican-dev/wings/config" "github.com/pelican-dev/wings/environment" "github.com/pelican-dev/wings/internal/models" - ) type CrashHandler struct { @@ -100,7 +99,7 @@ func (s *Server) handleServerCrash() error { "oomkilled": oomKilled, "logs": logs, }) - + s.crasher.SetLastCrash(time.Now()) return errors.Wrap(s.HandlePowerAction(PowerActionStart), "failed to start server after crash detection") diff --git a/server/filesystem/quotas/exfs.go b/server/filesystem/quotas/exfs.go index f0bfdc0e..24125b86 100644 --- a/server/filesystem/quotas/exfs.go +++ b/server/filesystem/quotas/exfs.go @@ -1,41 +1,172 @@ package quotas import ( + "fmt" + "html/template" "os" + "strings" - "github.com/g0rbe/go-chattr" + "github.com/parkervcp/fsquota" + "github.com/pelican-dev/wings/config" ) -// EnableEXFSQuota enables quotas on a specified folder -func EnableEXFSQuota(serverPath string) (err error) { - serverdir, err := os.OpenFile(serverPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) +var exfsProjects []exfsProject + +type exfsProject struct { + ID int + Name string + BasePath string +} + +const ( + projidTemplate = `{{ range . }}{{ .UUID }}:{{ .ID }} +{{ end }}` + projectsTemplate = `{{ range . }}{{ .ID }}:{{ .BasePath }}/{{ .UUID }} +{{ end }}` + + projidFile = `/etc/projid` + projectFile = `/etc/projects` +) + +// setQuota sets the quota in bytes for the specified server uuid +func (q exfsProject) setQuota(byteLimit uint64) (err error) { + serverProject, err := fsquota.LookupProject(q.Name) + if err != nil { + return + } + + serverDirPath := fmt.Sprintf("%s/%s", config.Get().System.Data, q.Name) + projInfo, err := fsquota.GetProjectInfo(serverDirPath, serverProject) if err != nil { return } - err = chattr.SetAttr(serverdir, chattr.FS_PROJINHERIT_FL) + projInfo.Limits.Bytes.SetHard(byteLimit) + + if _, err = fsquota.SetProjectQuota(serverDirPath, serverProject, projInfo.Limits); err != nil { + return + } return } -// DisableEXFSQuota disables quotas on a specified folder -func DisableEXFSQuota(serverPath string) (err error) { +// getQuota gets the specified quotas and usage of a specified server uuid +func (q exfsProject) getQuota() (bytesUsed int64, err error) { + serverProject, err := fsquota.LookupProject(q.Name) + if err != nil { + return + } + + projInfo, err := fsquota.GetProjectInfo(q.BasePath, serverProject) + if err != nil { + return + } + + // converts the uint64 to int64. + // This should only be an issue in the terms of exabytes... + return int64(projInfo.BytesUsed), nil +} + +// enableEXFSQuota enables quotas on a specified directory +func (q exfsProject) enableEXFSQuota() (err error) { + serverDir, err := os.OpenFile(fmt.Sprintf("%s/%s", q.BasePath, q.Name), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) + if err != nil { + return + } + + defer serverDir.Close() + + folderXattr, err := getXAttr(serverDir) + if err != nil { + return + } + + // ensure project inherit flag is set + if (folderXattr.XFlags & FS_XFLAG_PROJINHERIT) != 0 { + if err = setXAttr(serverDir, fsXAttr{XFlags: FS_XFLAG_PROJINHERIT}); err != nil { + return + } + } + + // ensure correct project id is set + if folderXattr.ProjectID != uint32(q.ID) { + if err = setXAttr(serverDir, fsXAttr{ProjectID: uint32(q.ID)}); err != nil { + return + } + } return } -// SetEXFSQuota sets the quota in bytes for the specified server uuid -func SetEXFSQuota(serverUUID string, byteLimit int64) (err error) { +func (q exfsProject) addProject() (err error) { + basePath := config.Get().System.Data + if strings.HasSuffix(basePath, "/") { + basePath = strings.TrimSuffix(config.Get().System.Data, "/") + } + + q.BasePath = basePath + exfsProjects = append(exfsProjects, q) + + if err = writeEXFSProjects(); err != nil { + return + } + + if err = q.enableEXFSQuota(); err != nil { + return + } return } -// GetEXFSQuota gets the specified quotas and usage of a specified server uuid -func GetEXFSQuota(serverUUID string) (byteLimit, bytesUsed int64, err error) { +// removeProject drops a specified project from the +func (q exfsProject) removeProject() (err error) { + for pos, project := range exfsProjects { + if project.Name == q.Name { + exfsProjects = append(exfsProjects[:pos], exfsProjects[pos+1:]...) + } + } + err = writeEXFSProjects() return } -// NewEXFSQuota -func NewEXFSQuota() (err error) { +func writeEXFSProjects() (err error) { + // write out projid file + idtmpl, err := template.New("projid").Parse(projidTemplate) + if err != nil { + return + } + + if err = writeTemplate(idtmpl, projidFile, exfsProjects); err != nil { + return + } + + projtmpl, err := template.New("projects").Parse(projectsTemplate) + if err != nil { + return + } + + if err = writeTemplate(projtmpl, projectFile, exfsProjects); err != nil { + return + } + return } + +func writeTemplate(t *template.Template, file string, data interface{}) (err error) { + f, err := os.Create(file) + if err != nil { + return err + } + + err = t.Execute(f, data) + if err != nil { + return err + } + + err = f.Close() + if err != nil { + return err + } + + return nil +} diff --git a/server/filesystem/quotas/functions.go b/server/filesystem/quotas/functions.go new file mode 100644 index 00000000..0badc1ae --- /dev/null +++ b/server/filesystem/quotas/functions.go @@ -0,0 +1,112 @@ +package quotas + +import ( + "syscall" + + "emperror.dev/errors" + "github.com/parkervcp/fsquota" + "github.com/pelican-dev/wings/config" +) + +const ( + FSBTRFS = 2435016766 + FSEXT4 = 61267 + FSXFS = 1481003842 + FSZFS = 801189825 +) + +var fstype string + +func getFSType(mount string) (fsType uint, err error) { + var stat syscall.Statfs_t + + if mount == "" { + return fsType, errors.New("must specify path to check the filesystem type") + } + + err = syscall.Statfs(mount, &stat) + if err != nil { + return fsType, err + } + + switch stat.Type { + case FSBTRFS: + return FSBTRFS, nil + case FSEXT4: + return FSEXT4, nil + case FSXFS: + return FSXFS, nil + case FSZFS: + return FSZFS, nil + default: + return fsType, errors.New("unknown filesystem type") + } +} + +// IsSupportedFS checks if the filesystem for the data files is supported. +// currently only EXT4 and XFS are supported +func IsSupportedFS() (err error) { + checked, err := getFSType(config.Get().System.Data) + if err != nil { + return err + } + + switch checked { + case FSEXT4 | FSXFS: + // technically tested on EXT4 and will need to be validated for XFS + supported, err := fsquota.ProjectQuotasSupported(config.Get().System.Data) + if err != nil { + return + } + if !supported { + return errors.New("project quotas not enabled") + } + fstype = "exfs" + return + case FSBTRFS: + fstype = "btrfs" + return errors.New("btrfs is not supported on this filesystem") + case FSZFS: + fstype = "zfs" + return errors.New("zfs is not supported on this filesystem") + default: + return errors.New("unknown filesystem type") + } +} + +// AddQuota adds a server to the configured quotas +func AddQuota(serverID int, serverUUID string) (err error) { + switch fstype { + case "exfs": + err = exfsProject{ID: serverID, Name: serverUUID}.addProject() + } + + return +} + +// DelQuota removes a server from the configured quotas +func DelQuota(serverUUID string) (err error) { + switch fstype { + case "exfs": + err = exfsProject{Name: serverUUID}.removeProject() + } + return +} + +// SetQuota configures quotas for a specified server +func SetQuota(limit int64, serverUUID string) (err error) { + switch fstype { + case "exfs": + err = exfsProject{Name: serverUUID}.setQuota(uint64(limit)) + } + return +} + +// GetQuota gets the data usage for a specified server +func GetQuota(serverUUID string) (used int64, err error) { + switch fstype { + case "exfs": + used, err = exfsProject{Name: serverUUID}.getQuota() + } + return +} diff --git a/server/filesystem/quotas/syscall_quota.go b/server/filesystem/quotas/syscall_quota.go deleted file mode 100644 index 74020d9f..00000000 --- a/server/filesystem/quotas/syscall_quota.go +++ /dev/null @@ -1,25 +0,0 @@ -package quotas - -const ( - Q_GETQUOTA = 0x0080000700 - Q_SETQUOTA = 0x0080000800 - Q_GETNEXTQUOTA = 0x00080000900 -) - -const ( - USRQUOTA = 0x0000000000 - GRPQUOTA = 0x0000000001 - PRJQUOTA = 0x0000000002 -) - -type DQBlk struct { - dqbBHardlimit uint64 - dqbBSoftlimit uint64 - dqbCurSpace uint64 - dqbIHardlimit uint64 - dqbISoftlimit uint64 - dqbCurInodes uint64 - dqbBTime uint64 - dqbITime uint64 - dqbValid uint32 -} diff --git a/server/filesystem/quotas/syscall_xattr.go b/server/filesystem/quotas/syscall_xattr.go index 5d3a72ec..f1ff6167 100644 --- a/server/filesystem/quotas/syscall_xattr.go +++ b/server/filesystem/quotas/syscall_xattr.go @@ -39,13 +39,13 @@ const ( */ const ( - FS_IOC_FSGETXATTR uintptr = 0x801c581f - FS_IOC_FSSETXATTR uintptr = 0x401c5820 + FS_IOC_FSGETXATTR uintptr = 0x801c581f // https://docs.rs/linux-raw-sys/latest/linux_raw_sys/ioctl/constant.FS_IOC_FSGETXATTR.html + FS_IOC_FSSETXATTR uintptr = 0x401c5820 // https://docs.rs/linux-raw-sys/latest/linux_raw_sys/ioctl/constant.FS_IOC_FSSETXATTR.html ) -// FSXAttr is the struct defining the structure +// fsXAttr is the struct defining the structure // for FS_IOC_FSGETXATTR and FS_IOC_FSSETXATTR -type FSXAttr struct { +type fsXAttr struct { XFlags uint32 ExtSize uint32 NextENTs uint32 @@ -54,32 +54,38 @@ type FSXAttr struct { } // xAttrCtl sets the -func xAttrCtl(f *os.File, request uintptr, xattr FSXAttr) error { - _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), request, uintptr(unsafe.Pointer(&xattr))) +func xAttrCtl(f *os.File, request uintptr, xattr *fsXAttr) (err error) { + attreq := uintptr(unsafe.Pointer(xattr)) + + _, _, errno := syscall.RawSyscall(syscall.SYS_IOCTL, f.Fd(), request, attreq) if errno != 0 { return os.NewSyscallError("ioctl", errno) } - return nil + return } // getXAttr gets the extended attributes of a file -func getXAttr(f *os.File) (xattr *FSXAttr, err error) { - xattr = new(FSXAttr) - - err = xAttrCtl(f, FS_IOC_FSGETXATTR, *xattr) - - if err != nil { - return xattr, err +func getXAttr(f *os.File) (attr fsXAttr, err error) { + if err = xAttrCtl(f, FS_IOC_FSGETXATTR, &attr); err != nil { + return } return } // setXAttr sets xattr values for the -func setXAttr(f *os.File, xattr *FSXAttr) (err error) { - err = xAttrCtl(f, FS_IOC_FSSETXATTR, *xattr) +func setXAttr(serverDir *os.File, fsXAttr fsXAttr) (err error) { + xAttr, err := getXAttr(serverDir) + if err != nil { + return err + } + + // bitwise add for uint32 X Attributes + xAttr.XFlags |= fsXAttr.XFlags + + err = xAttrCtl(serverDir, FS_IOC_FSSETXATTR, &fsXAttr) return } diff --git a/server/manager.go b/server/manager.go index 55dd645e..a9363478 100644 --- a/server/manager.go +++ b/server/manager.go @@ -14,12 +14,12 @@ import ( "github.com/apex/log" "github.com/gammazero/workerpool" "github.com/goccy/go-json" - "github.com/pelican-dev/wings/config" "github.com/pelican-dev/wings/environment" "github.com/pelican-dev/wings/environment/docker" "github.com/pelican-dev/wings/remote" "github.com/pelican-dev/wings/server/filesystem" + "github.com/pelican-dev/wings/server/filesystem/quotas" ) type Manager struct { @@ -192,7 +192,7 @@ func (m *Manager) InitServer(data remote.ServerConfigurationResponse) (*Server, // Setup the base server configuration data which will be used for all of the // remaining functionality in this call. - if err := s.SyncWithConfiguration(data); err != nil { + if err = s.SyncWithConfiguration(data); err != nil { return nil, errors.WithStackIf(err) } @@ -201,6 +201,17 @@ func (m *Manager) InitServer(data remote.ServerConfigurationResponse) (*Server, return nil, errors.WithStackIf(err) } + // if quotas are enabled ensure quotas are configured + if config.Get().System.Quotas.Enabled { + if err = quotas.AddQuota(s.cfg.ID, s.Config().Uuid); err != nil { + return nil, errors.WithStackIf(err) + } + + if err = quotas.SetQuota(s.Environment.Config().Limits().DiskSpace, s.Config().Uuid); err != nil { + return nil, errors.WithStackIf(err) + } + } + // Right now we only support a Docker based environment, so I'm going to hard code // this logic in. When we're ready to support other environment we'll need to make // some modifications here, obviously. diff --git a/server/power.go b/server/power.go index 622fd06c..a33de61d 100644 --- a/server/power.go +++ b/server/power.go @@ -6,10 +6,11 @@ import ( "time" "emperror.dev/errors" + "github.com/apex/log" "github.com/google/uuid" - "github.com/pelican-dev/wings/config" "github.com/pelican-dev/wings/environment" + "github.com/pelican-dev/wings/server/filesystem/quotas" ) type PowerAction string @@ -190,8 +191,25 @@ func (s *Server) onBeforeStart() error { s.Filesystem().HasSpaceAvailable(true) } else { s.PublishConsoleOutputFromDaemon("Checking server disk space usage, this could take a few seconds...") - if err := s.Filesystem().HasSpaceErr(false); err != nil { - return err + if config.Get().System.Quotas.Enabled { + // if quotas are enabled used the quota system to check usage + // ensure quotas are set before checking space used + if err := quotas.SetQuota(s.Environment.Config().Limits().DiskSpace, s.Config().Uuid); err != nil { + log.WithField("error", err).Error("failed to set quota for server") + } + + used, err := quotas.GetQuota(s.Config().Uuid) + if err != nil { + log.WithField("error", err).Error("failed to get quota for server") + } + + if used >= s.Environment.Config().Limits().DiskSpace { + return errors.New("quota for server is too large") + } + } else { + if err := s.Filesystem().HasSpaceErr(false); err != nil { + return err + } } } diff --git a/server/transfer/archive.go b/server/transfer/archive.go index 8f6cfc59..366750d4 100644 --- a/server/transfer/archive.go +++ b/server/transfer/archive.go @@ -37,10 +37,10 @@ func (t *Transfer) Archive() (*Archive, error) { func (a *Archive) StreamBackups(ctx context.Context, mp *multipart.Writer) error { // In theory this can't happen as this function is only called if there is at least 1 backup but just to be sure if len(a.transfer.BackupUUIDs) == 0 { - a.transfer.Log().Debug("no backups specified for transfer") - return nil - } - + a.transfer.Log().Debug("no backups specified for transfer") + return nil + } + cfg := config.Get() backupPath := filepath.Join(cfg.System.BackupDirectory, a.transfer.Server.ID()) @@ -55,27 +55,27 @@ func (a *Archive) StreamBackups(ctx context.Context, mp *multipart.Writer) error return err } - // Create a set of backup UUIDs for quick lookup - backupSet := make(map[string]bool) - for _, uuid := range a.transfer.BackupUUIDs { - backupSet[uuid+".tar.gz"] = true // Backup files are stored as UUID.tar.gz - } - - var backupsToTransfer []os.DirEntry - for _, entry := range entries { - if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".tar.gz") { - if backupSet[entry.Name()] { - backupsToTransfer = append(backupsToTransfer, entry) - } - } - } - - totalBackups := len(backupsToTransfer) - if totalBackups == 0 { - a.transfer.Log().Debug("no matching backup files found") - return nil - } - + // Create a set of backup UUIDs for quick lookup + backupSet := make(map[string]bool) + for _, uuid := range a.transfer.BackupUUIDs { + backupSet[uuid+".tar.gz"] = true // Backup files are stored as UUID.tar.gz + } + + var backupsToTransfer []os.DirEntry + for _, entry := range entries { + if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".tar.gz") { + if backupSet[entry.Name()] { + backupsToTransfer = append(backupsToTransfer, entry) + } + } + } + + totalBackups := len(backupsToTransfer) + if totalBackups == 0 { + a.transfer.Log().Debug("no matching backup files found") + return nil + } + a.transfer.Log().Infof("Starting transfer of %d backup files", totalBackups) a.transfer.SendMessage(fmt.Sprintf("Starting transfer of %d backup files", totalBackups)) From ce671bb32065633b14311f170aaf84c1e7ed652b Mon Sep 17 00:00:00 2001 From: "Michael (Parker) Parker" Date: Sun, 8 Feb 2026 13:05:02 -0500 Subject: [PATCH 06/15] fix issue with returns builds were failing due to un-used err --- server/filesystem/quotas/functions.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/filesystem/quotas/functions.go b/server/filesystem/quotas/functions.go index 0badc1ae..563da794 100644 --- a/server/filesystem/quotas/functions.go +++ b/server/filesystem/quotas/functions.go @@ -56,13 +56,14 @@ func IsSupportedFS() (err error) { // technically tested on EXT4 and will need to be validated for XFS supported, err := fsquota.ProjectQuotasSupported(config.Get().System.Data) if err != nil { - return + return err } if !supported { return errors.New("project quotas not enabled") } + fstype = "exfs" - return + return err case FSBTRFS: fstype = "btrfs" return errors.New("btrfs is not supported on this filesystem") From 5a4855db12c2d588765050971f5b02a47ec0a53f Mon Sep 17 00:00:00 2001 From: "Michael (Parker) Parker" Date: Wed, 14 Jan 2026 08:38:26 -0500 Subject: [PATCH 07/15] Start disk quota package --- go.mod | 1 + go.sum | 2 + server/filesystem/quotas/exfs.go | 41 +++++++++++ server/filesystem/quotas/syscall_quota.go | 25 +++++++ server/filesystem/quotas/syscall_xattr.go | 85 +++++++++++++++++++++++ 5 files changed, 154 insertions(+) create mode 100644 server/filesystem/quotas/exfs.go create mode 100644 server/filesystem/quotas/syscall_quota.go create mode 100644 server/filesystem/quotas/syscall_xattr.go diff --git a/go.mod b/go.mod index ef8aa589..f028eef3 100644 --- a/go.mod +++ b/go.mod @@ -64,6 +64,7 @@ require ( github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/g0rbe/go-chattr v1.0.1 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect diff --git a/go.sum b/go.sum index a560d283..7948c609 100644 --- a/go.sum +++ b/go.sum @@ -145,6 +145,8 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw github.com/franela/goblin v0.0.0-20211003143422-0a4f594942bf h1:NrF81UtW8gG2LBGkXFQFqlfNnvMt9WdB46sfdJY4oqc= github.com/franela/goblin v0.0.0-20211003143422-0a4f594942bf/go.mod h1:VzmDKDJVZI3aJmnRI9VjAn9nJ8qPPsN1fqzr9dqInIo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/g0rbe/go-chattr v1.0.1 h1:CHwYB+WKB46hkzt6Jxyvkyrz7u9njghUOFvmx2gir84= +github.com/g0rbe/go-chattr v1.0.1/go.mod h1:yQc6VPJfpDDC1g+W2t47+yYmzBNioax/GLiyJ25/IOs= github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gammazero/deque v0.2.1 h1:qSdsbG6pgp6nL7A0+K/B7s12mcCY/5l5SIUpMOl+dC0= diff --git a/server/filesystem/quotas/exfs.go b/server/filesystem/quotas/exfs.go new file mode 100644 index 00000000..f0bfdc0e --- /dev/null +++ b/server/filesystem/quotas/exfs.go @@ -0,0 +1,41 @@ +package quotas + +import ( + "os" + + "github.com/g0rbe/go-chattr" +) + +// EnableEXFSQuota enables quotas on a specified folder +func EnableEXFSQuota(serverPath string) (err error) { + serverdir, err := os.OpenFile(serverPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return + } + + err = chattr.SetAttr(serverdir, chattr.FS_PROJINHERIT_FL) + return +} + +// DisableEXFSQuota disables quotas on a specified folder +func DisableEXFSQuota(serverPath string) (err error) { + + return +} + +// SetEXFSQuota sets the quota in bytes for the specified server uuid +func SetEXFSQuota(serverUUID string, byteLimit int64) (err error) { + + return +} + +// GetEXFSQuota gets the specified quotas and usage of a specified server uuid +func GetEXFSQuota(serverUUID string) (byteLimit, bytesUsed int64, err error) { + + return +} + +// NewEXFSQuota +func NewEXFSQuota() (err error) { + return +} diff --git a/server/filesystem/quotas/syscall_quota.go b/server/filesystem/quotas/syscall_quota.go new file mode 100644 index 00000000..74020d9f --- /dev/null +++ b/server/filesystem/quotas/syscall_quota.go @@ -0,0 +1,25 @@ +package quotas + +const ( + Q_GETQUOTA = 0x0080000700 + Q_SETQUOTA = 0x0080000800 + Q_GETNEXTQUOTA = 0x00080000900 +) + +const ( + USRQUOTA = 0x0000000000 + GRPQUOTA = 0x0000000001 + PRJQUOTA = 0x0000000002 +) + +type DQBlk struct { + dqbBHardlimit uint64 + dqbBSoftlimit uint64 + dqbCurSpace uint64 + dqbIHardlimit uint64 + dqbISoftlimit uint64 + dqbCurInodes uint64 + dqbBTime uint64 + dqbITime uint64 + dqbValid uint32 +} diff --git a/server/filesystem/quotas/syscall_xattr.go b/server/filesystem/quotas/syscall_xattr.go new file mode 100644 index 00000000..5d3a72ec --- /dev/null +++ b/server/filesystem/quotas/syscall_xattr.go @@ -0,0 +1,85 @@ +package quotas + +import ( + "os" + "syscall" + "unsafe" +) + +// Pulled definitions from /usr/include/linux/fs.h + +/* + * Flags for the fsx_xflags field + */ +const ( + FS_XFLAG_REALTIME = 0x00000001 /* data in realtime volume */ + FS_XFLAG_PREALLOC = 0x00000002 /* preallocated file extents */ + FS_XFLAG_IMMUTABLE = 0x00000008 /* file cannot be modified */ + FS_XFLAG_APPEND = 0x00000010 /* all writes append */ + FS_XFLAG_SYNC = 0x00000020 /* all writes synchronous */ + FS_XFLAG_NOATIME = 0x00000040 /* do not update access time */ + FS_XFLAG_NODUMP = 0x00000080 /* do not include in backups */ + FS_XFLAG_RTINHERIT = 0x00000100 /* create with rt bit set */ + FS_XFLAG_PROJINHERIT = 0x00000200 /* create with parents projid */ + FS_XFLAG_NOSYMLINKS = 0x00000400 /* disallow symlink creation */ + FS_XFLAG_EXTSIZE = 0x00000800 /* extent size allocator hint */ + FS_XFLAG_EXTSZINHERIT = 0x00001000 /* inherit inode extent size */ + FS_XFLAG_NODEFRAG = 0x00002000 /* do not defragment */ + FS_XFLAG_FILESTREAM = 0x00004000 /* use filestream allocator */ + FS_XFLAG_DAX = 0x00008000 /* use DAX for IO */ + FS_XFLAG_COWEXTSIZE = 0x00010000 /* CoW extent size allocator hint */ + FS_XFLAG_HASATTR = 0x80000000 /* no DIFLAG for this */ +) + +/* +#define FS_IOC_GETFLAGS _IOR('f', 1, long) +#define FS_IOC_SETFLAGS _IOW('f', 2, long) +#define FS_IOC_FSGETXATTR _IOR('X', 31, struct fsxattr) +#define FS_IOC_FSSETXATTR _IOW('X', 32, struct fsxattr) +*/ + +const ( + FS_IOC_FSGETXATTR uintptr = 0x801c581f + FS_IOC_FSSETXATTR uintptr = 0x401c5820 +) + +// FSXAttr is the struct defining the structure +// for FS_IOC_FSGETXATTR and FS_IOC_FSSETXATTR +type FSXAttr struct { + XFlags uint32 + ExtSize uint32 + NextENTs uint32 + ProjectID uint32 + FSXPad byte +} + +// xAttrCtl sets the +func xAttrCtl(f *os.File, request uintptr, xattr FSXAttr) error { + _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), request, uintptr(unsafe.Pointer(&xattr))) + + if errno != 0 { + return os.NewSyscallError("ioctl", errno) + } + + return nil +} + +// getXAttr gets the extended attributes of a file +func getXAttr(f *os.File) (xattr *FSXAttr, err error) { + xattr = new(FSXAttr) + + err = xAttrCtl(f, FS_IOC_FSGETXATTR, *xattr) + + if err != nil { + return xattr, err + } + + return +} + +// setXAttr sets xattr values for the +func setXAttr(f *os.File, xattr *FSXAttr) (err error) { + err = xAttrCtl(f, FS_IOC_FSSETXATTR, *xattr) + + return +} From fadb07111e3f4d93def732de1e1e6335b820cea0 Mon Sep 17 00:00:00 2001 From: "Michael (Parker) Parker" Date: Sat, 7 Feb 2026 15:25:17 -0500 Subject: [PATCH 08/15] Add filesystem based disk quotas This uses the built in quota management services to manage quotas for EXT4. May potentially work on XFS but has not been tested. See the docs for more information. https://pelican.dev/docs/guides/disk-quotas/about --- cmd/root.go | 18 +++ config/config.go | 5 + go.mod | 9 +- go.sum | 8 ++ router/router_server.go | 11 +- router/router_server_files.go | 2 +- router/router_transfer.go | 21 ++- server/activity.go | 1 - server/configuration.go | 4 + server/crash.go | 3 +- server/filesystem/quotas/exfs.go | 157 ++++++++++++++++++++-- server/filesystem/quotas/functions.go | 112 +++++++++++++++ server/filesystem/quotas/syscall_quota.go | 25 ---- server/filesystem/quotas/syscall_xattr.go | 38 +++--- server/manager.go | 15 ++- server/power.go | 24 +++- server/transfer/archive.go | 50 +++---- 17 files changed, 399 insertions(+), 104 deletions(-) create mode 100644 server/filesystem/quotas/functions.go delete mode 100644 server/filesystem/quotas/syscall_quota.go diff --git a/cmd/root.go b/cmd/root.go index c615b0b4..35c36e56 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -23,6 +23,7 @@ import ( "github.com/docker/docker/client" "github.com/gammazero/workerpool" "github.com/mitchellh/colorstring" + "github.com/pelican-dev/wings/server/filesystem/quotas" "github.com/spf13/cobra" "golang.org/x/crypto/acme" "golang.org/x/crypto/acme/autocert" @@ -191,6 +192,23 @@ func rootCmdRun(cmd *cobra.Command, _ []string) { log.WithField("server", s.ID()).Info("finished loading configuration for server") } + // if quotas are enabled ensure they are added and enabled. + if config.Get().System.Quotas.Enabled { + // check if the fs is supported + if err = quotas.IsSupportedFS(); err != nil { + log.WithField("error", err).Fatal("failed to validate quota configuration") + } + + // validate all servers are configured for quotas + for _, s := range manager.All() { + if err = quotas.AddQuota(s.Config().ID, s.Config().Uuid); err != nil { + log.WithField("error", err).Error("failed to add server to quota list") + } + } + + log.Info("quotas configured") + } + states, err := manager.ReadStates() if err != nil { log.WithField("error", err).Error("failed to retrieve locally cached server states from disk, assuming all servers in offline state") diff --git a/config/config.go b/config/config.go index cf0cd38d..78890f75 100644 --- a/config/config.go +++ b/config/config.go @@ -214,6 +214,11 @@ type SystemConfiguration struct { // disk usage is not a concern. DiskCheckInterval int64 `default:"150" yaml:"disk_check_interval"` + // Quotas define is quota management is enabled on the Data directory + Quotas struct { + Enabled bool `json:"enabled" yaml:"enabled" default:"false"` + } `json:"quotas" yaml:"quotas"` + // ActivitySendInterval is the amount of time that should ellapse between aggregated server activity // being sent to the Panel. By default this will send activity collected over the last minute. Keep // in mind that only a fixed number of activity log entries, defined by ActivitySendCount, will be sent diff --git a/go.mod b/go.mod index f028eef3..5287b817 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/docker/go-connections v0.6.0 github.com/fatih/color v1.18.0 github.com/franela/goblin v0.0.0-20211003143422-0a4f594942bf + github.com/g0rbe/go-chattr v1.0.1 github.com/gabriel-vasile/mimetype v1.4.10 github.com/gammazero/workerpool v1.1.3 github.com/gbrlsnchs/jwt/v3 v3.0.1 @@ -34,6 +35,7 @@ require ( github.com/mattn/go-colorable v0.1.14 github.com/mholt/archives v0.1.3 github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db + github.com/parkervcp/fsquota v0.0.0-20240729140958-47d273ab9426 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pkg/sftp v1.13.9 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 @@ -64,12 +66,13 @@ require ( github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/g0rbe/go-chattr v1.0.1 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mikelolasagasti/xz v1.0.1 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect + github.com/moby/sys/mountinfo v0.7.1 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect @@ -95,7 +98,7 @@ require ( github.com/containerd/errdefs v0.3.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect @@ -138,7 +141,7 @@ require ( github.com/pelletier/go-toml/v2 v2.2.2 github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/robfig/cron/v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 7948c609..1cc9f82f 100644 --- a/go.sum +++ b/go.sum @@ -120,6 +120,7 @@ github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbD github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM= @@ -229,6 +230,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= @@ -315,6 +318,8 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/mountinfo v0.7.1 h1:/tTvQaSJRr2FshkhXiIpux6fQ2Zvc4j7tAhMTStAG2g= +github.com/moby/sys/mountinfo v0.7.1/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae h1:O4SWKdcHVCvYqyDV+9CJA1fcDN2L11Bule0iFy3YlAI= @@ -342,6 +347,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/parkervcp/fsquota v0.0.0-20240729140958-47d273ab9426 h1:4JbdZ5mDEEz1FkH48AZsn68MGgcEwzJbYUyXeRErGk0= +github.com/parkervcp/fsquota v0.0.0-20240729140958-47d273ab9426/go.mod h1:8T5p+Jn1ZK/bz/F+QBxaMrzjpi3/lCjISQjUdVSW+DQ= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= @@ -355,6 +362,7 @@ github.com/pkg/sftp v1.13.9 h1:4NGkvGudBL7GteO3m6qnaQ4pC0Kvf0onSVc9gR3EWBw= github.com/pkg/sftp v1.13.9/go.mod h1:OBN7bVXdstkFFN/gdnHPUb5TE8eb8G1Rp9wCItqjkkA= 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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= diff --git a/router/router_server.go b/router/router_server.go index 19a1cb75..c7afd969 100644 --- a/router/router_server.go +++ b/router/router_server.go @@ -8,11 +8,11 @@ import ( "strconv" "strings" - "github.com/pelican-dev/wings/config" - "emperror.dev/errors" "github.com/apex/log" "github.com/gin-gonic/gin" + "github.com/pelican-dev/wings/config" + "github.com/pelican-dev/wings/server/filesystem/quotas" "github.com/pelican-dev/wings/router/downloader" "github.com/pelican-dev/wings/router/middleware" @@ -283,6 +283,13 @@ func deleteServer(c *gin.Context) { log.WithFields(log.Fields{"path": p, "error": err}). Warn("failed to remove server files during deletion process") } + + if config.Get().System.Quotas.Enabled { + if err = quotas.DelQuota(s.Config().Uuid); err != nil { + log.WithFields(log.Fields{"server_id": s.Config().ID, "error": err}). + Warn("failed to remove quota during deletion process") + } + } }(s) // remove hanging machine-id file for the server when removing diff --git a/router/router_server_files.go b/router/router_server_files.go index 481b7916..98d0b20b 100644 --- a/router/router_server_files.go +++ b/router/router_server_files.go @@ -240,7 +240,7 @@ func postServerDeleteFiles(c *gin.Context) { return s.Filesystem().SafeDeleteRecursively(pi) } }) - + } if err := g.Wait(); err != nil { diff --git a/router/router_transfer.go b/router/router_transfer.go index f49ef709..8ea9a5cd 100644 --- a/router/router_transfer.go +++ b/router/router_transfer.go @@ -142,14 +142,13 @@ func postTransfers(c *gin.Context) { return } - // Used to read the file and checksum from the request body. mr := multipart.NewReader(c.Request.Body, params["boundary"]) var ( - hasArchive bool - archiveChecksum string - archiveChecksumReceived string + hasArchive bool + archiveChecksum string + archiveChecksumReceived string backupChecksumsCalculated = make(map[string]string) backupChecksumsReceived = make(map[string]string) ) @@ -208,7 +207,7 @@ out: case name == "install_logs": trnsfr.Log().Debug("received install logs") - + // Create install log directory if it doesn't exist cfg := config.Get() installLogDir := filepath.Join(cfg.System.LogDirectory, "install") @@ -217,10 +216,10 @@ out: trnsfr.Log().WithError(err).Warn("failed to create install log directory, skipping") break } - + // Use the correct install log path with server UUID installLogPath := filepath.Join(installLogDir, trnsfr.Server.ID()+".log") - + // Create the install log file installLogFile, err := os.Create(installLogPath) if err != nil { @@ -228,7 +227,7 @@ out: trnsfr.Log().WithError(err).Warn("failed to create install log file, skipping") break } - + // Stream the install logs to file if _, err := io.Copy(installLogFile, p); err != nil { installLogFile.Close() @@ -236,14 +235,14 @@ out: trnsfr.Log().WithError(err).Warn("failed to stream install logs to file, skipping") break } - + if err := installLogFile.Close(); err != nil { // Don't fail transfer for install logs, just log and continue trnsfr.Log().WithError(err).Warn("failed to close install log file") } - + trnsfr.Log().WithField("path", installLogPath).Debug("install logs saved successfully") - + case strings.HasPrefix(name, "backup_"): backupName := strings.TrimPrefix(name, "backup_") trnsfr.Log().WithField("backup", backupName).Debug("received backup file") diff --git a/server/activity.go b/server/activity.go index 584e40c4..271d022f 100644 --- a/server/activity.go +++ b/server/activity.go @@ -21,7 +21,6 @@ const ( ActivitySftpDelete = models.Event("server:sftp.delete") ActivityFileUploaded = models.Event("server:file.uploaded") ActivityServerCrashed = models.Event("server:crashed") - ) // RequestActivity is a wrapper around a LoggedEvent that is able to track additional request diff --git a/server/configuration.go b/server/configuration.go index 15d74a56..753584da 100644 --- a/server/configuration.go +++ b/server/configuration.go @@ -46,6 +46,10 @@ type ConfigurationMeta struct { type Configuration struct { mu sync.RWMutex + // ID is the database id from the panel that is guaranteed to be unique + // this is being used for quotas + ID int `json:"id"` + // The unique identifier for the server that should be used when referencing // it against the Panel API (and internally). This will be used when naming // docker containers as well as in log output. diff --git a/server/crash.go b/server/crash.go index 3be6587e..2e7b3ee0 100644 --- a/server/crash.go +++ b/server/crash.go @@ -12,7 +12,6 @@ import ( "github.com/pelican-dev/wings/config" "github.com/pelican-dev/wings/environment" "github.com/pelican-dev/wings/internal/models" - ) type CrashHandler struct { @@ -100,7 +99,7 @@ func (s *Server) handleServerCrash() error { "oomkilled": oomKilled, "logs": logs, }) - + s.crasher.SetLastCrash(time.Now()) return errors.Wrap(s.HandlePowerAction(PowerActionStart), "failed to start server after crash detection") diff --git a/server/filesystem/quotas/exfs.go b/server/filesystem/quotas/exfs.go index f0bfdc0e..24125b86 100644 --- a/server/filesystem/quotas/exfs.go +++ b/server/filesystem/quotas/exfs.go @@ -1,41 +1,172 @@ package quotas import ( + "fmt" + "html/template" "os" + "strings" - "github.com/g0rbe/go-chattr" + "github.com/parkervcp/fsquota" + "github.com/pelican-dev/wings/config" ) -// EnableEXFSQuota enables quotas on a specified folder -func EnableEXFSQuota(serverPath string) (err error) { - serverdir, err := os.OpenFile(serverPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) +var exfsProjects []exfsProject + +type exfsProject struct { + ID int + Name string + BasePath string +} + +const ( + projidTemplate = `{{ range . }}{{ .UUID }}:{{ .ID }} +{{ end }}` + projectsTemplate = `{{ range . }}{{ .ID }}:{{ .BasePath }}/{{ .UUID }} +{{ end }}` + + projidFile = `/etc/projid` + projectFile = `/etc/projects` +) + +// setQuota sets the quota in bytes for the specified server uuid +func (q exfsProject) setQuota(byteLimit uint64) (err error) { + serverProject, err := fsquota.LookupProject(q.Name) + if err != nil { + return + } + + serverDirPath := fmt.Sprintf("%s/%s", config.Get().System.Data, q.Name) + projInfo, err := fsquota.GetProjectInfo(serverDirPath, serverProject) if err != nil { return } - err = chattr.SetAttr(serverdir, chattr.FS_PROJINHERIT_FL) + projInfo.Limits.Bytes.SetHard(byteLimit) + + if _, err = fsquota.SetProjectQuota(serverDirPath, serverProject, projInfo.Limits); err != nil { + return + } return } -// DisableEXFSQuota disables quotas on a specified folder -func DisableEXFSQuota(serverPath string) (err error) { +// getQuota gets the specified quotas and usage of a specified server uuid +func (q exfsProject) getQuota() (bytesUsed int64, err error) { + serverProject, err := fsquota.LookupProject(q.Name) + if err != nil { + return + } + + projInfo, err := fsquota.GetProjectInfo(q.BasePath, serverProject) + if err != nil { + return + } + + // converts the uint64 to int64. + // This should only be an issue in the terms of exabytes... + return int64(projInfo.BytesUsed), nil +} + +// enableEXFSQuota enables quotas on a specified directory +func (q exfsProject) enableEXFSQuota() (err error) { + serverDir, err := os.OpenFile(fmt.Sprintf("%s/%s", q.BasePath, q.Name), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) + if err != nil { + return + } + + defer serverDir.Close() + + folderXattr, err := getXAttr(serverDir) + if err != nil { + return + } + + // ensure project inherit flag is set + if (folderXattr.XFlags & FS_XFLAG_PROJINHERIT) != 0 { + if err = setXAttr(serverDir, fsXAttr{XFlags: FS_XFLAG_PROJINHERIT}); err != nil { + return + } + } + + // ensure correct project id is set + if folderXattr.ProjectID != uint32(q.ID) { + if err = setXAttr(serverDir, fsXAttr{ProjectID: uint32(q.ID)}); err != nil { + return + } + } return } -// SetEXFSQuota sets the quota in bytes for the specified server uuid -func SetEXFSQuota(serverUUID string, byteLimit int64) (err error) { +func (q exfsProject) addProject() (err error) { + basePath := config.Get().System.Data + if strings.HasSuffix(basePath, "/") { + basePath = strings.TrimSuffix(config.Get().System.Data, "/") + } + + q.BasePath = basePath + exfsProjects = append(exfsProjects, q) + + if err = writeEXFSProjects(); err != nil { + return + } + + if err = q.enableEXFSQuota(); err != nil { + return + } return } -// GetEXFSQuota gets the specified quotas and usage of a specified server uuid -func GetEXFSQuota(serverUUID string) (byteLimit, bytesUsed int64, err error) { +// removeProject drops a specified project from the +func (q exfsProject) removeProject() (err error) { + for pos, project := range exfsProjects { + if project.Name == q.Name { + exfsProjects = append(exfsProjects[:pos], exfsProjects[pos+1:]...) + } + } + err = writeEXFSProjects() return } -// NewEXFSQuota -func NewEXFSQuota() (err error) { +func writeEXFSProjects() (err error) { + // write out projid file + idtmpl, err := template.New("projid").Parse(projidTemplate) + if err != nil { + return + } + + if err = writeTemplate(idtmpl, projidFile, exfsProjects); err != nil { + return + } + + projtmpl, err := template.New("projects").Parse(projectsTemplate) + if err != nil { + return + } + + if err = writeTemplate(projtmpl, projectFile, exfsProjects); err != nil { + return + } + return } + +func writeTemplate(t *template.Template, file string, data interface{}) (err error) { + f, err := os.Create(file) + if err != nil { + return err + } + + err = t.Execute(f, data) + if err != nil { + return err + } + + err = f.Close() + if err != nil { + return err + } + + return nil +} diff --git a/server/filesystem/quotas/functions.go b/server/filesystem/quotas/functions.go new file mode 100644 index 00000000..0badc1ae --- /dev/null +++ b/server/filesystem/quotas/functions.go @@ -0,0 +1,112 @@ +package quotas + +import ( + "syscall" + + "emperror.dev/errors" + "github.com/parkervcp/fsquota" + "github.com/pelican-dev/wings/config" +) + +const ( + FSBTRFS = 2435016766 + FSEXT4 = 61267 + FSXFS = 1481003842 + FSZFS = 801189825 +) + +var fstype string + +func getFSType(mount string) (fsType uint, err error) { + var stat syscall.Statfs_t + + if mount == "" { + return fsType, errors.New("must specify path to check the filesystem type") + } + + err = syscall.Statfs(mount, &stat) + if err != nil { + return fsType, err + } + + switch stat.Type { + case FSBTRFS: + return FSBTRFS, nil + case FSEXT4: + return FSEXT4, nil + case FSXFS: + return FSXFS, nil + case FSZFS: + return FSZFS, nil + default: + return fsType, errors.New("unknown filesystem type") + } +} + +// IsSupportedFS checks if the filesystem for the data files is supported. +// currently only EXT4 and XFS are supported +func IsSupportedFS() (err error) { + checked, err := getFSType(config.Get().System.Data) + if err != nil { + return err + } + + switch checked { + case FSEXT4 | FSXFS: + // technically tested on EXT4 and will need to be validated for XFS + supported, err := fsquota.ProjectQuotasSupported(config.Get().System.Data) + if err != nil { + return + } + if !supported { + return errors.New("project quotas not enabled") + } + fstype = "exfs" + return + case FSBTRFS: + fstype = "btrfs" + return errors.New("btrfs is not supported on this filesystem") + case FSZFS: + fstype = "zfs" + return errors.New("zfs is not supported on this filesystem") + default: + return errors.New("unknown filesystem type") + } +} + +// AddQuota adds a server to the configured quotas +func AddQuota(serverID int, serverUUID string) (err error) { + switch fstype { + case "exfs": + err = exfsProject{ID: serverID, Name: serverUUID}.addProject() + } + + return +} + +// DelQuota removes a server from the configured quotas +func DelQuota(serverUUID string) (err error) { + switch fstype { + case "exfs": + err = exfsProject{Name: serverUUID}.removeProject() + } + return +} + +// SetQuota configures quotas for a specified server +func SetQuota(limit int64, serverUUID string) (err error) { + switch fstype { + case "exfs": + err = exfsProject{Name: serverUUID}.setQuota(uint64(limit)) + } + return +} + +// GetQuota gets the data usage for a specified server +func GetQuota(serverUUID string) (used int64, err error) { + switch fstype { + case "exfs": + used, err = exfsProject{Name: serverUUID}.getQuota() + } + return +} diff --git a/server/filesystem/quotas/syscall_quota.go b/server/filesystem/quotas/syscall_quota.go deleted file mode 100644 index 74020d9f..00000000 --- a/server/filesystem/quotas/syscall_quota.go +++ /dev/null @@ -1,25 +0,0 @@ -package quotas - -const ( - Q_GETQUOTA = 0x0080000700 - Q_SETQUOTA = 0x0080000800 - Q_GETNEXTQUOTA = 0x00080000900 -) - -const ( - USRQUOTA = 0x0000000000 - GRPQUOTA = 0x0000000001 - PRJQUOTA = 0x0000000002 -) - -type DQBlk struct { - dqbBHardlimit uint64 - dqbBSoftlimit uint64 - dqbCurSpace uint64 - dqbIHardlimit uint64 - dqbISoftlimit uint64 - dqbCurInodes uint64 - dqbBTime uint64 - dqbITime uint64 - dqbValid uint32 -} diff --git a/server/filesystem/quotas/syscall_xattr.go b/server/filesystem/quotas/syscall_xattr.go index 5d3a72ec..f1ff6167 100644 --- a/server/filesystem/quotas/syscall_xattr.go +++ b/server/filesystem/quotas/syscall_xattr.go @@ -39,13 +39,13 @@ const ( */ const ( - FS_IOC_FSGETXATTR uintptr = 0x801c581f - FS_IOC_FSSETXATTR uintptr = 0x401c5820 + FS_IOC_FSGETXATTR uintptr = 0x801c581f // https://docs.rs/linux-raw-sys/latest/linux_raw_sys/ioctl/constant.FS_IOC_FSGETXATTR.html + FS_IOC_FSSETXATTR uintptr = 0x401c5820 // https://docs.rs/linux-raw-sys/latest/linux_raw_sys/ioctl/constant.FS_IOC_FSSETXATTR.html ) -// FSXAttr is the struct defining the structure +// fsXAttr is the struct defining the structure // for FS_IOC_FSGETXATTR and FS_IOC_FSSETXATTR -type FSXAttr struct { +type fsXAttr struct { XFlags uint32 ExtSize uint32 NextENTs uint32 @@ -54,32 +54,38 @@ type FSXAttr struct { } // xAttrCtl sets the -func xAttrCtl(f *os.File, request uintptr, xattr FSXAttr) error { - _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), request, uintptr(unsafe.Pointer(&xattr))) +func xAttrCtl(f *os.File, request uintptr, xattr *fsXAttr) (err error) { + attreq := uintptr(unsafe.Pointer(xattr)) + + _, _, errno := syscall.RawSyscall(syscall.SYS_IOCTL, f.Fd(), request, attreq) if errno != 0 { return os.NewSyscallError("ioctl", errno) } - return nil + return } // getXAttr gets the extended attributes of a file -func getXAttr(f *os.File) (xattr *FSXAttr, err error) { - xattr = new(FSXAttr) - - err = xAttrCtl(f, FS_IOC_FSGETXATTR, *xattr) - - if err != nil { - return xattr, err +func getXAttr(f *os.File) (attr fsXAttr, err error) { + if err = xAttrCtl(f, FS_IOC_FSGETXATTR, &attr); err != nil { + return } return } // setXAttr sets xattr values for the -func setXAttr(f *os.File, xattr *FSXAttr) (err error) { - err = xAttrCtl(f, FS_IOC_FSSETXATTR, *xattr) +func setXAttr(serverDir *os.File, fsXAttr fsXAttr) (err error) { + xAttr, err := getXAttr(serverDir) + if err != nil { + return err + } + + // bitwise add for uint32 X Attributes + xAttr.XFlags |= fsXAttr.XFlags + + err = xAttrCtl(serverDir, FS_IOC_FSSETXATTR, &fsXAttr) return } diff --git a/server/manager.go b/server/manager.go index 55dd645e..a9363478 100644 --- a/server/manager.go +++ b/server/manager.go @@ -14,12 +14,12 @@ import ( "github.com/apex/log" "github.com/gammazero/workerpool" "github.com/goccy/go-json" - "github.com/pelican-dev/wings/config" "github.com/pelican-dev/wings/environment" "github.com/pelican-dev/wings/environment/docker" "github.com/pelican-dev/wings/remote" "github.com/pelican-dev/wings/server/filesystem" + "github.com/pelican-dev/wings/server/filesystem/quotas" ) type Manager struct { @@ -192,7 +192,7 @@ func (m *Manager) InitServer(data remote.ServerConfigurationResponse) (*Server, // Setup the base server configuration data which will be used for all of the // remaining functionality in this call. - if err := s.SyncWithConfiguration(data); err != nil { + if err = s.SyncWithConfiguration(data); err != nil { return nil, errors.WithStackIf(err) } @@ -201,6 +201,17 @@ func (m *Manager) InitServer(data remote.ServerConfigurationResponse) (*Server, return nil, errors.WithStackIf(err) } + // if quotas are enabled ensure quotas are configured + if config.Get().System.Quotas.Enabled { + if err = quotas.AddQuota(s.cfg.ID, s.Config().Uuid); err != nil { + return nil, errors.WithStackIf(err) + } + + if err = quotas.SetQuota(s.Environment.Config().Limits().DiskSpace, s.Config().Uuid); err != nil { + return nil, errors.WithStackIf(err) + } + } + // Right now we only support a Docker based environment, so I'm going to hard code // this logic in. When we're ready to support other environment we'll need to make // some modifications here, obviously. diff --git a/server/power.go b/server/power.go index 622fd06c..a33de61d 100644 --- a/server/power.go +++ b/server/power.go @@ -6,10 +6,11 @@ import ( "time" "emperror.dev/errors" + "github.com/apex/log" "github.com/google/uuid" - "github.com/pelican-dev/wings/config" "github.com/pelican-dev/wings/environment" + "github.com/pelican-dev/wings/server/filesystem/quotas" ) type PowerAction string @@ -190,8 +191,25 @@ func (s *Server) onBeforeStart() error { s.Filesystem().HasSpaceAvailable(true) } else { s.PublishConsoleOutputFromDaemon("Checking server disk space usage, this could take a few seconds...") - if err := s.Filesystem().HasSpaceErr(false); err != nil { - return err + if config.Get().System.Quotas.Enabled { + // if quotas are enabled used the quota system to check usage + // ensure quotas are set before checking space used + if err := quotas.SetQuota(s.Environment.Config().Limits().DiskSpace, s.Config().Uuid); err != nil { + log.WithField("error", err).Error("failed to set quota for server") + } + + used, err := quotas.GetQuota(s.Config().Uuid) + if err != nil { + log.WithField("error", err).Error("failed to get quota for server") + } + + if used >= s.Environment.Config().Limits().DiskSpace { + return errors.New("quota for server is too large") + } + } else { + if err := s.Filesystem().HasSpaceErr(false); err != nil { + return err + } } } diff --git a/server/transfer/archive.go b/server/transfer/archive.go index 8f6cfc59..366750d4 100644 --- a/server/transfer/archive.go +++ b/server/transfer/archive.go @@ -37,10 +37,10 @@ func (t *Transfer) Archive() (*Archive, error) { func (a *Archive) StreamBackups(ctx context.Context, mp *multipart.Writer) error { // In theory this can't happen as this function is only called if there is at least 1 backup but just to be sure if len(a.transfer.BackupUUIDs) == 0 { - a.transfer.Log().Debug("no backups specified for transfer") - return nil - } - + a.transfer.Log().Debug("no backups specified for transfer") + return nil + } + cfg := config.Get() backupPath := filepath.Join(cfg.System.BackupDirectory, a.transfer.Server.ID()) @@ -55,27 +55,27 @@ func (a *Archive) StreamBackups(ctx context.Context, mp *multipart.Writer) error return err } - // Create a set of backup UUIDs for quick lookup - backupSet := make(map[string]bool) - for _, uuid := range a.transfer.BackupUUIDs { - backupSet[uuid+".tar.gz"] = true // Backup files are stored as UUID.tar.gz - } - - var backupsToTransfer []os.DirEntry - for _, entry := range entries { - if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".tar.gz") { - if backupSet[entry.Name()] { - backupsToTransfer = append(backupsToTransfer, entry) - } - } - } - - totalBackups := len(backupsToTransfer) - if totalBackups == 0 { - a.transfer.Log().Debug("no matching backup files found") - return nil - } - + // Create a set of backup UUIDs for quick lookup + backupSet := make(map[string]bool) + for _, uuid := range a.transfer.BackupUUIDs { + backupSet[uuid+".tar.gz"] = true // Backup files are stored as UUID.tar.gz + } + + var backupsToTransfer []os.DirEntry + for _, entry := range entries { + if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".tar.gz") { + if backupSet[entry.Name()] { + backupsToTransfer = append(backupsToTransfer, entry) + } + } + } + + totalBackups := len(backupsToTransfer) + if totalBackups == 0 { + a.transfer.Log().Debug("no matching backup files found") + return nil + } + a.transfer.Log().Infof("Starting transfer of %d backup files", totalBackups) a.transfer.SendMessage(fmt.Sprintf("Starting transfer of %d backup files", totalBackups)) From c2e97a32bbc52ede66a1d4f346b3726997bc4bcb Mon Sep 17 00:00:00 2001 From: "Michael (Parker) Parker" Date: Sun, 8 Feb 2026 13:05:02 -0500 Subject: [PATCH 09/15] fix issue with returns builds were failing due to un-used err --- server/filesystem/quotas/functions.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/filesystem/quotas/functions.go b/server/filesystem/quotas/functions.go index 0badc1ae..563da794 100644 --- a/server/filesystem/quotas/functions.go +++ b/server/filesystem/quotas/functions.go @@ -56,13 +56,14 @@ func IsSupportedFS() (err error) { // technically tested on EXT4 and will need to be validated for XFS supported, err := fsquota.ProjectQuotasSupported(config.Get().System.Data) if err != nil { - return + return err } if !supported { return errors.New("project quotas not enabled") } + fstype = "exfs" - return + return err case FSBTRFS: fstype = "btrfs" return errors.New("btrfs is not supported on this filesystem") From f265de9d39da7874463b52aca90fbfcd76ecbd20 Mon Sep 17 00:00:00 2001 From: "Michael (Parker) Parker" Date: Tue, 17 Feb 2026 12:51:09 -0500 Subject: [PATCH 10/15] update quota management update internal name for the panel id to Pid to avoid overlap with ID as the uuid moves quota checking earlier to avoid an out of order problem add logging to quota management update detection of supported filesystems update how directory xattr is handled --- cmd/root.go | 30 +++--- go.mod | 2 +- router/router_server.go | 4 +- server/configuration.go | 10 +- server/filesystem/quotas/exfs.go | 67 +++++++------- server/filesystem/quotas/functions.go | 108 +++++++++++++--------- server/filesystem/quotas/syscall_xattr.go | 50 +++++----- server/manager.go | 7 +- server/power.go | 34 ++++--- server/resources.go | 16 +++- server/server.go | 16 +++- 11 files changed, 186 insertions(+), 158 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 35c36e56..e3636507 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -168,6 +168,17 @@ func rootCmdRun(cmd *cobra.Command, _ []string) { return } + // if quotas are enabled ensure they are added and enabled. + if config.Get().System.Quotas.Enabled { + log.Info("validating system is configured for quotas") + // check if the fs is supported + if !quotas.IsSupportedFS() { + log.Fatal("failed to validate quota configuration") + } + + log.Info("quotas are supported and enabled") + } + manager, err := server.NewManager(cmd.Context(), pclient) if err != nil { log.WithField("error", err).Fatal("failed to load server configurations") @@ -189,24 +200,7 @@ func rootCmdRun(cmd *cobra.Command, _ []string) { // Just for some nice log output. for _, s := range manager.All() { - log.WithField("server", s.ID()).Info("finished loading configuration for server") - } - - // if quotas are enabled ensure they are added and enabled. - if config.Get().System.Quotas.Enabled { - // check if the fs is supported - if err = quotas.IsSupportedFS(); err != nil { - log.WithField("error", err).Fatal("failed to validate quota configuration") - } - - // validate all servers are configured for quotas - for _, s := range manager.All() { - if err = quotas.AddQuota(s.Config().ID, s.Config().Uuid); err != nil { - log.WithField("error", err).Error("failed to add server to quota list") - } - } - - log.Info("quotas configured") + log.WithField("server", s.ID()).Info("finished loading configuration") } states, err := manager.ReadStates() diff --git a/go.mod b/go.mod index 5287b817..6e537716 100644 --- a/go.mod +++ b/go.mod @@ -43,6 +43,7 @@ require ( github.com/spf13/cobra v1.10.1 github.com/stretchr/testify v1.11.1 github.com/tidwall/gjson v1.18.0 + github.com/tidwall/pretty v1.2.0 github.com/tidwall/sjson v1.2.5 golang.org/x/crypto v0.41.0 golang.org/x/sync v0.16.0 @@ -78,7 +79,6 @@ require ( github.com/muesli/termenv v0.16.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/tidwall/match v1.1.1 // indirect - github.com/tidwall/pretty v1.2.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect ) diff --git a/router/router_server.go b/router/router_server.go index c7afd969..152a4581 100644 --- a/router/router_server.go +++ b/router/router_server.go @@ -81,7 +81,7 @@ func getServerInstallLogs(c *gin.Context) { // request until a potentially slow operation completes. // // This is done because for the most part the Panel is using websockets to determine when -// things are happening, so theres no reason to sit and wait for a request to finish. We'll +// things are happening, so there's no reason to sit and wait for a request to finish. We'll // just see over the socket if something isn't working correctly. func postServerPower(c *gin.Context) { s := ExtractServer(c) @@ -286,7 +286,7 @@ func deleteServer(c *gin.Context) { if config.Get().System.Quotas.Enabled { if err = quotas.DelQuota(s.Config().Uuid); err != nil { - log.WithFields(log.Fields{"server_id": s.Config().ID, "error": err}). + log.WithFields(log.Fields{"server_id": s.Config().Pid, "error": err}). Warn("failed to remove quota during deletion process") } } diff --git a/server/configuration.go b/server/configuration.go index 753584da..41bd51da 100644 --- a/server/configuration.go +++ b/server/configuration.go @@ -46,9 +46,9 @@ type ConfigurationMeta struct { type Configuration struct { mu sync.RWMutex - // ID is the database id from the panel that is guaranteed to be unique + // P_ID is the database id from the panel that is guaranteed to be unique // this is being used for quotas - ID int `json:"id"` + Pid int `json:"id"` // The unique identifier for the server that should be used when referencing // it against the Panel API (and internally). This will be used when naming @@ -112,6 +112,12 @@ func (c *Configuration) GetUuid() string { return c.Uuid } +func (c *Configuration) GetPID() int { + c.mu.RLock() + defer c.mu.RUnlock() + return c.Pid +} + func (c *Configuration) SetSuspended(s bool) { c.mu.Lock() defer c.mu.Unlock() diff --git a/server/filesystem/quotas/exfs.go b/server/filesystem/quotas/exfs.go index 24125b86..50b1ead0 100644 --- a/server/filesystem/quotas/exfs.go +++ b/server/filesystem/quotas/exfs.go @@ -6,6 +6,8 @@ import ( "os" "strings" + "emperror.dev/errors" + "github.com/apex/log" "github.com/parkervcp/fsquota" "github.com/pelican-dev/wings/config" ) @@ -19,9 +21,9 @@ type exfsProject struct { } const ( - projidTemplate = `{{ range . }}{{ .UUID }}:{{ .ID }} + projidTemplate = `{{ range . }}{{ .Name }}:{{ .ID }} {{ end }}` - projectsTemplate = `{{ range . }}{{ .ID }}:{{ .BasePath }}/{{ .UUID }} + projectsTemplate = `{{ range . }}{{ .ID }}:{{ .BasePath }}/{{ .Name }} {{ end }}` projidFile = `/etc/projid` @@ -30,22 +32,22 @@ const ( // setQuota sets the quota in bytes for the specified server uuid func (q exfsProject) setQuota(byteLimit uint64) (err error) { + log.WithFields(log.Fields{"server-path": fmt.Sprintf("%s/%s", q.BasePath, q.Name)}).Debug("setting quota") serverProject, err := fsquota.LookupProject(q.Name) if err != nil { return } serverDirPath := fmt.Sprintf("%s/%s", config.Get().System.Data, q.Name) - projInfo, err := fsquota.GetProjectInfo(serverDirPath, serverProject) - if err != nil { - return - } + limits := fsquota.Limits{} - projInfo.Limits.Bytes.SetHard(byteLimit) + limits.Bytes.SetHard(byteLimit) - if _, err = fsquota.SetProjectQuota(serverDirPath, serverProject, projInfo.Limits); err != nil { + if _, err = fsquota.SetProjectQuota(serverDirPath, serverProject, limits); err != nil { return } + + log.WithFields(log.Fields{"server-path": fmt.Sprintf("%s/%s", q.BasePath, q.Name)}).Debug("quota set") return } @@ -53,14 +55,13 @@ func (q exfsProject) setQuota(byteLimit uint64) (err error) { func (q exfsProject) getQuota() (bytesUsed int64, err error) { serverProject, err := fsquota.LookupProject(q.Name) if err != nil { - return + return -1, err } projInfo, err := fsquota.GetProjectInfo(q.BasePath, serverProject) if err != nil { - return + return -1, err } - // converts the uint64 to int64. // This should only be an issue in the terms of exabytes... return int64(projInfo.BytesUsed), nil @@ -68,42 +69,28 @@ func (q exfsProject) getQuota() (bytesUsed int64, err error) { // enableEXFSQuota enables quotas on a specified directory func (q exfsProject) enableEXFSQuota() (err error) { - serverDir, err := os.OpenFile(fmt.Sprintf("%s/%s", q.BasePath, q.Name), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) + serverDir, err := os.Open(fmt.Sprintf("%s/%s", q.BasePath, q.Name)) if err != nil { - return + return err } defer serverDir.Close() - folderXattr, err := getXAttr(serverDir) - if err != nil { + // enable project quota inheritance and set project id + if err = setXAttr(serverDir, q.ID, FS_XFLAG_PROJINHERIT); err != nil { + log.WithFields(log.Fields{"server-uuid": q.Name, "server-path": serverDir.Name()}).Error("failed to update XATTRs for server") return } - // ensure project inherit flag is set - if (folderXattr.XFlags & FS_XFLAG_PROJINHERIT) != 0 { - if err = setXAttr(serverDir, fsXAttr{XFlags: FS_XFLAG_PROJINHERIT}); err != nil { - return - } - } - - // ensure correct project id is set - if folderXattr.ProjectID != uint32(q.ID) { - if err = setXAttr(serverDir, fsXAttr{ProjectID: uint32(q.ID)}); err != nil { - return - } - } - return } func (q exfsProject) addProject() (err error) { - basePath := config.Get().System.Data - if strings.HasSuffix(basePath, "/") { - basePath = strings.TrimSuffix(config.Get().System.Data, "/") + q.BasePath = config.Get().System.Data + if strings.HasSuffix(q.BasePath, "/") { + q.BasePath = strings.TrimSuffix(config.Get().System.Data, "/") } - q.BasePath = basePath exfsProjects = append(exfsProjects, q) if err = writeEXFSProjects(); err != nil { @@ -129,11 +116,21 @@ func (q exfsProject) removeProject() (err error) { return } +func getProject(serverUUID string) (serverProject exfsProject, err error) { + for _, project := range exfsProjects { + if project.Name == serverUUID { + return project, nil + } + } + + return serverProject, errors.New("quota for server doesn't exist") +} + func writeEXFSProjects() (err error) { // write out projid file idtmpl, err := template.New("projid").Parse(projidTemplate) if err != nil { - return + return err } if err = writeTemplate(idtmpl, projidFile, exfsProjects); err != nil { @@ -142,7 +139,7 @@ func writeEXFSProjects() (err error) { projtmpl, err := template.New("projects").Parse(projectsTemplate) if err != nil { - return + return err } if err = writeTemplate(projtmpl, projectFile, exfsProjects); err != nil { diff --git a/server/filesystem/quotas/functions.go b/server/filesystem/quotas/functions.go index 563da794..8c821fd9 100644 --- a/server/filesystem/quotas/functions.go +++ b/server/filesystem/quotas/functions.go @@ -4,110 +4,126 @@ import ( "syscall" "emperror.dev/errors" + "github.com/apex/log" "github.com/parkervcp/fsquota" "github.com/pelican-dev/wings/config" ) const ( - FSBTRFS = 2435016766 - FSEXT4 = 61267 - FSXFS = 1481003842 - FSZFS = 801189825 + FSBTRFS int64 = 2435016766 + FSEXT4 int64 = 61267 + FSXFS int64 = 1481003842 + FSZFS int64 = 801189825 ) -var fstype string +var fsType int64 -func getFSType(mount string) (fsType uint, err error) { +func getFSType(mount string) (err error) { var stat syscall.Statfs_t if mount == "" { - return fsType, errors.New("must specify path to check the filesystem type") + return errors.New("must specify path to check the filesystem type") } err = syscall.Statfs(mount, &stat) if err != nil { - return fsType, err + return err } - switch stat.Type { + fsType = stat.Type + + switch fsType { case FSBTRFS: - return FSBTRFS, nil + log.WithField("fs-type", "brtfs").Debug("found filesystem") + return nil case FSEXT4: - return FSEXT4, nil + log.WithField("fs-type", "ext4").Debug("found filesystem") + return nil case FSXFS: - return FSXFS, nil + log.WithField("fs-type", "xfs").Debug("found filesystem") + return nil case FSZFS: - return FSZFS, nil + log.WithField("fs-type", "zfs").Debug("found filesystem") + return nil default: - return fsType, errors.New("unknown filesystem type") + return errors.New("unknown filesystem type") } } // IsSupportedFS checks if the filesystem for the data files is supported. // currently only EXT4 and XFS are supported -func IsSupportedFS() (err error) { - checked, err := getFSType(config.Get().System.Data) +func IsSupportedFS() (supported bool) { + log.WithField("path", config.Get().System.Data).Debug("checking filesystem type") + err := getFSType(config.Get().System.Data) if err != nil { - return err + log.Error(err.Error()) + return } - switch checked { - case FSEXT4 | FSXFS: + if fsType == FSEXT4 || fsType == FSXFS { // technically tested on EXT4 and will need to be validated for XFS - supported, err := fsquota.ProjectQuotasSupported(config.Get().System.Data) + supported, err = fsquota.ProjectQuotasSupported(config.Get().System.Data) if err != nil { - return err + return } + if !supported { - return errors.New("project quotas not enabled") + log.WithField("path", config.Get().System.Data).Error("quotas are not enabled") + return } - - fstype = "exfs" - return err - case FSBTRFS: - fstype = "btrfs" - return errors.New("btrfs is not supported on this filesystem") - case FSZFS: - fstype = "zfs" - return errors.New("zfs is not supported on this filesystem") - default: - return errors.New("unknown filesystem type") + log.Debug("using kernel based quota management") + } else if fsType == FSBTRFS { + log.WithField("path", config.Get().System.Data).Error("btrfs is not supported") + } else if fsType == FSZFS { + log.WithField("path", config.Get().System.Data).Error("btrfs is not supported") } + + return } // AddQuota adds a server to the configured quotas func AddQuota(serverID int, serverUUID string) (err error) { - switch fstype { - case "exfs": - err = exfsProject{ID: serverID, Name: serverUUID}.addProject() + log.Debug("adding server to stored quota projects") + if fsType == FSEXT4 || fsType == FSXFS { + return exfsProject{ID: serverID, Name: serverUUID, BasePath: config.Get().System.Data}.addProject() } - return + return errors.New("failed to set a quota") } // DelQuota removes a server from the configured quotas func DelQuota(serverUUID string) (err error) { - switch fstype { - case "exfs": - err = exfsProject{Name: serverUUID}.removeProject() + if fsType == FSEXT4 || fsType == FSXFS { + fsProject, err := getProject(serverUUID) + if err != nil { + return err + } + return fsProject.removeProject() } return } // SetQuota configures quotas for a specified server func SetQuota(limit int64, serverUUID string) (err error) { - switch fstype { - case "exfs": - err = exfsProject{Name: serverUUID}.setQuota(uint64(limit)) + log.WithField("server", serverUUID).Debug("setting quota") + if fsType == FSEXT4 || fsType == FSXFS { + fsProject, err := getProject(serverUUID) + if err != nil { + return err + } + return fsProject.setQuota(uint64(limit)) } return } // GetQuota gets the data usage for a specified server func GetQuota(serverUUID string) (used int64, err error) { - switch fstype { - case "exfs": - used, err = exfsProject{Name: serverUUID}.getQuota() + if fsType == FSEXT4 || fsType == FSXFS { + fsProject, err := getProject(serverUUID) + if err != nil { + return used, err + } + return fsProject.getQuota() } return } diff --git a/server/filesystem/quotas/syscall_xattr.go b/server/filesystem/quotas/syscall_xattr.go index f1ff6167..0f14e365 100644 --- a/server/filesystem/quotas/syscall_xattr.go +++ b/server/filesystem/quotas/syscall_xattr.go @@ -12,23 +12,23 @@ import ( * Flags for the fsx_xflags field */ const ( - FS_XFLAG_REALTIME = 0x00000001 /* data in realtime volume */ - FS_XFLAG_PREALLOC = 0x00000002 /* preallocated file extents */ - FS_XFLAG_IMMUTABLE = 0x00000008 /* file cannot be modified */ - FS_XFLAG_APPEND = 0x00000010 /* all writes append */ - FS_XFLAG_SYNC = 0x00000020 /* all writes synchronous */ - FS_XFLAG_NOATIME = 0x00000040 /* do not update access time */ - FS_XFLAG_NODUMP = 0x00000080 /* do not include in backups */ - FS_XFLAG_RTINHERIT = 0x00000100 /* create with rt bit set */ - FS_XFLAG_PROJINHERIT = 0x00000200 /* create with parents projid */ - FS_XFLAG_NOSYMLINKS = 0x00000400 /* disallow symlink creation */ - FS_XFLAG_EXTSIZE = 0x00000800 /* extent size allocator hint */ - FS_XFLAG_EXTSZINHERIT = 0x00001000 /* inherit inode extent size */ - FS_XFLAG_NODEFRAG = 0x00002000 /* do not defragment */ - FS_XFLAG_FILESTREAM = 0x00004000 /* use filestream allocator */ - FS_XFLAG_DAX = 0x00008000 /* use DAX for IO */ - FS_XFLAG_COWEXTSIZE = 0x00010000 /* CoW extent size allocator hint */ - FS_XFLAG_HASATTR = 0x80000000 /* no DIFLAG for this */ + FS_XFLAG_REALTIME uint32 = 0x00000001 /* data in realtime volume */ + FS_XFLAG_PREALLOC uint32 = 0x00000002 /* preallocated file extents */ + FS_XFLAG_IMMUTABLE uint32 = 0x00000008 /* file cannot be modified */ + FS_XFLAG_APPEND uint32 = 0x00000010 /* all writes append */ + FS_XFLAG_SYNC uint32 = 0x00000020 /* all writes synchronous */ + FS_XFLAG_NOATIME uint32 = 0x00000040 /* do not update access time */ + FS_XFLAG_NODUMP uint32 = 0x00000080 /* do not include in backups */ + FS_XFLAG_RTINHERIT uint32 = 0x00000100 /* create with rt bit set */ + FS_XFLAG_PROJINHERIT uint32 = 0x00000200 /* create with parents projid */ + FS_XFLAG_NOSYMLINKS uint32 = 0x00000400 /* disallow symlink creation */ + FS_XFLAG_EXTSIZE uint32 = 0x00000800 /* extent size allocator hint */ + FS_XFLAG_EXTSZINHERIT uint32 = 0x00001000 /* inherit inode extent size */ + FS_XFLAG_NODEFRAG uint32 = 0x00002000 /* do not defragment */ + FS_XFLAG_FILESTREAM uint32 = 0x00004000 /* use filestream allocator */ + FS_XFLAG_DAX uint32 = 0x00008000 /* use DAX for IO */ + FS_XFLAG_COWEXTSIZE uint32 = 0x00010000 /* CoW extent size allocator hint */ + FS_XFLAG_HASATTR uint32 = 0x80000000 /* no DIFLAG for this */ ) /* @@ -55,10 +55,9 @@ type fsXAttr struct { // xAttrCtl sets the func xAttrCtl(f *os.File, request uintptr, xattr *fsXAttr) (err error) { - attreq := uintptr(unsafe.Pointer(xattr)) - - _, _, errno := syscall.RawSyscall(syscall.SYS_IOCTL, f.Fd(), request, attreq) + xattreq := uintptr(unsafe.Pointer(xattr)) + _, _, errno := syscall.RawSyscall(syscall.SYS_IOCTL, f.Fd(), request, xattreq) if errno != 0 { return os.NewSyscallError("ioctl", errno) } @@ -76,16 +75,15 @@ func getXAttr(f *os.File) (attr fsXAttr, err error) { } // setXAttr sets xattr values for the -func setXAttr(serverDir *os.File, fsXAttr fsXAttr) (err error) { - xAttr, err := getXAttr(serverDir) +func setXAttr(f *os.File, projectID int, attr uint32) (err error) { + fxattr, err := getXAttr(f) if err != nil { return err } - // bitwise add for uint32 X Attributes - xAttr.XFlags |= fsXAttr.XFlags - - err = xAttrCtl(serverDir, FS_IOC_FSSETXATTR, &fsXAttr) + fxattr.XFlags |= attr + fxattr.ProjectID = uint32(projectID) + err = xAttrCtl(f, FS_IOC_FSSETXATTR, &fxattr) return } diff --git a/server/manager.go b/server/manager.go index a9363478..e6dd3a7f 100644 --- a/server/manager.go +++ b/server/manager.go @@ -203,11 +203,7 @@ func (m *Manager) InitServer(data remote.ServerConfigurationResponse) (*Server, // if quotas are enabled ensure quotas are configured if config.Get().System.Quotas.Enabled { - if err = quotas.AddQuota(s.cfg.ID, s.Config().Uuid); err != nil { - return nil, errors.WithStackIf(err) - } - - if err = quotas.SetQuota(s.Environment.Config().Limits().DiskSpace, s.Config().Uuid); err != nil { + if err = quotas.AddQuota(s.Config().Pid, s.Config().Uuid); err != nil { return nil, errors.WithStackIf(err) } } @@ -260,7 +256,6 @@ func (m *Manager) init(ctx context.Context) error { pool := workerpool.New(runtime.NumCPU()) log.Debugf("using %d workerpools to instantiate server instances", runtime.NumCPU()) for _, data := range servers { - data := data pool.Submit(func() { // Parse the json.RawMessage into an expected struct value. We do this here so that a single broken // server does not cause the entire boot process to hang, and allows us to show more useful error diff --git a/server/power.go b/server/power.go index a33de61d..0d6668e8 100644 --- a/server/power.go +++ b/server/power.go @@ -187,26 +187,24 @@ func (s *Server) onBeforeStart() error { // If a server has unlimited disk space, we don't care enough to block the startup to check remaining. // However, we should trigger a size anyway, as it'd be good to kick it off for other processes. - if s.DiskSpace() <= 0 { - s.Filesystem().HasSpaceAvailable(true) - } else { - s.PublishConsoleOutputFromDaemon("Checking server disk space usage, this could take a few seconds...") - if config.Get().System.Quotas.Enabled { - // if quotas are enabled used the quota system to check usage - // ensure quotas are set before checking space used - if err := quotas.SetQuota(s.Environment.Config().Limits().DiskSpace, s.Config().Uuid); err != nil { - log.WithField("error", err).Error("failed to set quota for server") - } - - used, err := quotas.GetQuota(s.Config().Uuid) - if err != nil { - log.WithField("error", err).Error("failed to get quota for server") - } + if config.Get().System.Quotas.Enabled { + s.PublishConsoleOutputFromDaemon("checking disk space via quota, just a second") + // get used disk space + used, err := quotas.GetQuota(s.Config().Uuid) + if err != nil { + log.WithField("error", err).Error("failed to get quota for server") + return err + } - if used >= s.Environment.Config().Limits().DiskSpace { - return errors.New("quota for server is too large") - } + // used space is greater than the configured disk space + if used >= s.Environment.Config().Limits().DiskSpace { + return errors.New("currently used disk space is more than allocated") + } + } else { + if s.DiskSpace() <= 0 { + s.Filesystem().HasSpaceAvailable(true) } else { + s.PublishConsoleOutputFromDaemon("checking server disk space usage, this could take a few seconds...") if err := s.Filesystem().HasSpaceErr(false); err != nil { return err } diff --git a/server/resources.go b/server/resources.go index 7da1181e..65992155 100644 --- a/server/resources.go +++ b/server/resources.go @@ -4,7 +4,10 @@ import ( "sync" "sync/atomic" + "github.com/apex/log" + "github.com/pelican-dev/wings/config" "github.com/pelican-dev/wings/environment" + "github.com/pelican-dev/wings/server/filesystem/quotas" "github.com/pelican-dev/wings/system" ) @@ -32,8 +35,17 @@ type ResourceUsage struct { func (s *Server) Proc() ResourceUsage { s.resources.mu.Lock() defer s.resources.mu.Unlock() - // Store the updated disk usage when requesting process usage. - atomic.StoreInt64(&s.resources.Disk, s.Filesystem().CachedUsage()) + if config.Get().System.Quotas.Enabled { + used, err := quotas.GetQuota(s.ID()) + if err != nil { + log.WithFields(log.Fields{"server-uuid": s.ID(), "error": err.Error()}).Error("there was an issue getting the used disk space") + } + atomic.StoreInt64(&s.resources.Disk, used) + } else { + // Store the updated disk usage when requesting process usage. + atomic.StoreInt64(&s.resources.Disk, s.Filesystem().CachedUsage()) + } + //goland:noinspection GoVetCopyLock return s.resources } diff --git a/server/server.go b/server/server.go index c1e4c3fc..2a6fcd6d 100644 --- a/server/server.go +++ b/server/server.go @@ -17,6 +17,7 @@ import ( "github.com/apex/log" "github.com/creasty/defaults" "github.com/goccy/go-json" + "github.com/pelican-dev/wings/server/filesystem/quotas" "github.com/pelican-dev/wings/config" "github.com/pelican-dev/wings/environment" @@ -125,6 +126,11 @@ func (s *Server) ID() string { return s.Config().GetUuid() } +// PID returns the Panel DBID for the server instance. +func (s *Server) PID() int { + return s.Config().GetPID() +} + // Id returns the UUID for the server instance. This function is deprecated // in favor of Server.ID(). // @@ -142,7 +148,7 @@ func (s *Server) CtxCancel() { } } -// Returns a context instance for the server. This should be used to allow background +// Context returns a context instance for the server. This should be used to allow background // tasks to be canceled if the server is removed. It will only be canceled when the // application is stopped or if the server gets deleted. func (s *Server) Context() context.Context { @@ -264,7 +270,13 @@ func (s *Server) Sync() error { // Update the disk space limits for the server whenever the configuration for // it changes. - s.fs.SetDiskLimit(s.DiskSpace()) + if config.Get().System.Quotas.Enabled { + if err = quotas.SetQuota(s.DiskSpace(), s.ID()); err != nil { + return err + } + } else { + s.fs.SetDiskLimit(s.DiskSpace()) + } s.SyncWithEnvironment() From 3c38eac7e5b9c572520bf7f88871537b08e57db5 Mon Sep 17 00:00:00 2001 From: "Michael (Parker) Parker" Date: Wed, 18 Feb 2026 23:07:05 -0500 Subject: [PATCH 11/15] update quota setting code --- server/filesystem/quotas/exfs.go | 3 ++- server/power.go | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/filesystem/quotas/exfs.go b/server/filesystem/quotas/exfs.go index 50b1ead0..4700c184 100644 --- a/server/filesystem/quotas/exfs.go +++ b/server/filesystem/quotas/exfs.go @@ -32,7 +32,7 @@ const ( // setQuota sets the quota in bytes for the specified server uuid func (q exfsProject) setQuota(byteLimit uint64) (err error) { - log.WithFields(log.Fields{"server-path": fmt.Sprintf("%s/%s", q.BasePath, q.Name)}).Debug("setting quota") + log.WithFields(log.Fields{"server-path": fmt.Sprintf("%s/%s", q.BasePath, q.Name), "bytes": byteLimit}).Debug("setting quota") serverProject, err := fsquota.LookupProject(q.Name) if err != nil { return @@ -62,6 +62,7 @@ func (q exfsProject) getQuota() (bytesUsed int64, err error) { if err != nil { return -1, err } + // converts the uint64 to int64. // This should only be an issue in the terms of exabytes... return int64(projInfo.BytesUsed), nil diff --git a/server/power.go b/server/power.go index 0d6668e8..921cd7de 100644 --- a/server/power.go +++ b/server/power.go @@ -195,9 +195,8 @@ func (s *Server) onBeforeStart() error { log.WithField("error", err).Error("failed to get quota for server") return err } - // used space is greater than the configured disk space - if used >= s.Environment.Config().Limits().DiskSpace { + if used >= s.DiskSpace() { return errors.New("currently used disk space is more than allocated") } } else { From f977c45d1ca22fc4b21785333f945f018e1096c5 Mon Sep 17 00:00:00 2001 From: "Michael (Parker) Parker" Date: Sat, 28 Feb 2026 10:23:54 -0500 Subject: [PATCH 12/15] replace syscall with sys/unix use the updated and recommended "golang.org/x/sys/unix" package per https://go.googlesource.com/proposal/+/refs/heads/master/design/freeze-syscall.md --- cmd/root.go | 4 ++-- internal/ufs/error.go | 11 +++++------ server/filesystem/quotas/functions.go | 6 +++--- server/filesystem/quotas/syscall_xattr.go | 5 +++-- server/filesystem/stat_linux.go | 3 +-- system/system.go | 11 ++++++----- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index e3636507..b26a330d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -14,7 +14,6 @@ import ( "runtime" "strconv" "strings" - "syscall" "time" "github.com/NYTimes/logrotate" @@ -27,6 +26,7 @@ import ( "github.com/spf13/cobra" "golang.org/x/crypto/acme" "golang.org/x/crypto/acme/autocert" + "golang.org/x/sys/unix" "github.com/pelican-dev/wings/config" "github.com/pelican-dev/wings/environment" @@ -191,7 +191,7 @@ func rootCmdRun(cmd *cobra.Command, _ []string) { } if err := config.WriteToDisk(config.Get()); err != nil { - if !errors.Is(err, syscall.EROFS) { + if !errors.Is(err, unix.EROFS) { log.WithField("error", err).Error("failed to write configuration to disk") } else { log.WithField("error", err).Debug("failed to write configuration to disk") diff --git a/internal/ufs/error.go b/internal/ufs/error.go index 842d404f..3e02546b 100644 --- a/internal/ufs/error.go +++ b/internal/ufs/error.go @@ -7,7 +7,6 @@ import ( "errors" iofs "io/fs" "os" - "syscall" "golang.org/x/sys/unix" ) @@ -65,7 +64,7 @@ func convertErrorType(err error) error { var pErr *PathError if errors.As(err, &pErr) { - if errno, ok := pErr.Err.(syscall.Errno); ok { + if errno, ok := pErr.Err.(unix.Errno); ok { return errnoToPathError(errno, pErr.Op, pErr.Path) } return pErr @@ -74,7 +73,7 @@ func convertErrorType(err error) error { // If the error wasn't already a path error and is a errno, wrap it with // details that we can use to know there is something wrong with our // error wrapping somewhere. - var errno syscall.Errno + var errno unix.Errno if errors.As(err, &errno) { return &PathError{ Op: "!(UNKNOWN)", @@ -100,7 +99,7 @@ func ensurePathError(err error, op, path string) error { // // DO NOT USE `errors.As` or whatever here, the error will either be // an errno, or it will be wrapped already. - if errno, ok := pErr.Err.(syscall.Errno); ok { + if errno, ok := pErr.Err.(unix.Errno); ok { return errnoToPathError(errno, pErr.Op, pErr.Path) } // Return the PathError as-is without modification. @@ -108,7 +107,7 @@ func ensurePathError(err error, op, path string) error { } // If the error is directly an errno, convert it to a PathError. - var errno syscall.Errno + var errno unix.Errno if errors.As(err, &errno) { return errnoToPathError(errno, op, path) } @@ -122,7 +121,7 @@ func ensurePathError(err error, op, path string) error { } // errnoToPathError converts an errno into a proper path error. -func errnoToPathError(err syscall.Errno, op, path string) error { +func errnoToPathError(err unix.Errno, op, path string) error { switch err { // File exists case unix.EEXIST: diff --git a/server/filesystem/quotas/functions.go b/server/filesystem/quotas/functions.go index 8c821fd9..30ba015b 100644 --- a/server/filesystem/quotas/functions.go +++ b/server/filesystem/quotas/functions.go @@ -1,7 +1,7 @@ package quotas import ( - "syscall" + "golang.org/x/sys/unix" "emperror.dev/errors" "github.com/apex/log" @@ -19,13 +19,13 @@ const ( var fsType int64 func getFSType(mount string) (err error) { - var stat syscall.Statfs_t + var stat unix.Statfs_t if mount == "" { return errors.New("must specify path to check the filesystem type") } - err = syscall.Statfs(mount, &stat) + err = unix.Statfs(mount, &stat) if err != nil { return err } diff --git a/server/filesystem/quotas/syscall_xattr.go b/server/filesystem/quotas/syscall_xattr.go index 0f14e365..19f2fe1f 100644 --- a/server/filesystem/quotas/syscall_xattr.go +++ b/server/filesystem/quotas/syscall_xattr.go @@ -2,8 +2,9 @@ package quotas import ( "os" - "syscall" "unsafe" + + "golang.org/x/sys/unix" ) // Pulled definitions from /usr/include/linux/fs.h @@ -57,7 +58,7 @@ type fsXAttr struct { func xAttrCtl(f *os.File, request uintptr, xattr *fsXAttr) (err error) { xattreq := uintptr(unsafe.Pointer(xattr)) - _, _, errno := syscall.RawSyscall(syscall.SYS_IOCTL, f.Fd(), request, xattreq) + _, _, errno := unix.RawSyscall(unix.SYS_IOCTL, f.Fd(), request, xattreq) if errno != 0 { return os.NewSyscallError("ioctl", errno) } diff --git a/server/filesystem/stat_linux.go b/server/filesystem/stat_linux.go index 7891bafb..8d62e5e1 100644 --- a/server/filesystem/stat_linux.go +++ b/server/filesystem/stat_linux.go @@ -1,7 +1,6 @@ package filesystem import ( - "syscall" "time" "golang.org/x/sys/unix" @@ -16,7 +15,7 @@ func (s *Stat) CTime() time.Time { // Do not remove these "redundant" type-casts, they are required for 32-bit builds to work. return time.Unix(int64(st.Ctim.Sec), int64(st.Ctim.Nsec)) } - if st, ok := s.Sys().(*syscall.Stat_t); ok { + if st, ok := s.Sys().(*unix.Stat_t); ok { // Do not remove these "redundant" type-casts, they are required for 32-bit builds to work. return time.Unix(int64(st.Ctim.Sec), int64(st.Ctim.Nsec)) } diff --git a/system/system.go b/system/system.go index 87ffc56b..f0ccecd9 100644 --- a/system/system.go +++ b/system/system.go @@ -5,7 +5,8 @@ import ( "net" "runtime" "strings" - "syscall" + + "golang.org/x/sys/unix" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/image" @@ -184,14 +185,14 @@ func GetSystemIps() ([]string, error) { // getDiskForPath finds the mountpoint where the given path is stored func getDiskForPath(path string, partitions []disk.PartitionStat) (string, string, error) { - var stat syscall.Statfs_t - if err := syscall.Statfs(path, &stat); err != nil { + var stat unix.Statfs_t + if err := unix.Statfs(path, &stat); err != nil { return "", "", err } for _, part := range partitions { - var pStat syscall.Statfs_t - if err := syscall.Statfs(part.Mountpoint, &pStat); err != nil { + var pStat unix.Statfs_t + if err := unix.Statfs(part.Mountpoint, &pStat); err != nil { continue } if stat.Fsid == pStat.Fsid { From 329850a4774c72747caec69bd0d6a1f1e304c19b Mon Sep 17 00:00:00 2001 From: "Michael (Parker) Parker" Date: Mon, 2 Mar 2026 08:42:23 -0500 Subject: [PATCH 13/15] update go mod --- go.mod | 170 +++++++++++++++++++++++++----------------------- go.sum | 199 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 289 insertions(+), 80 deletions(-) diff --git a/go.mod b/go.mod index 6e537716..a34c2ce3 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/pelican-dev/wings -go 1.24.0 +go 1.25.0 require ( emperror.dev/errors v0.8.1 @@ -12,90 +12,100 @@ require ( github.com/buger/jsonparser v1.1.1 github.com/cenkalti/backoff/v4 v4.3.0 github.com/creasty/defaults v1.8.0 - github.com/docker/docker v28.5.1+incompatible + github.com/docker/docker v28.5.2+incompatible github.com/docker/go-connections v0.6.0 github.com/fatih/color v1.18.0 github.com/franela/goblin v0.0.0-20211003143422-0a4f594942bf github.com/g0rbe/go-chattr v1.0.1 - github.com/gabriel-vasile/mimetype v1.4.10 - github.com/gammazero/workerpool v1.1.3 + github.com/gabriel-vasile/mimetype v1.4.13 + github.com/gammazero/workerpool v1.2.1 github.com/gbrlsnchs/jwt/v3 v3.0.1 - github.com/gin-gonic/gin v1.10.1 + github.com/gin-gonic/gin v1.12.0 github.com/glebarez/sqlite v1.11.0 - github.com/go-co-op/gocron/v2 v2.16.5 + github.com/go-co-op/gocron/v2 v2.19.1 github.com/goccy/go-json v0.10.5 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/iancoleman/strcase v0.3.0 github.com/icza/dyno v0.0.0-20230330125955-09f820a8d9c0 github.com/juju/ratelimit v1.0.2 - github.com/klauspost/compress v1.18.0 + github.com/klauspost/compress v1.18.4 github.com/klauspost/pgzip v1.2.6 github.com/magiconair/properties v1.8.10 github.com/mattn/go-colorable v0.1.14 - github.com/mholt/archives v0.1.3 + github.com/mholt/archives v0.1.5 github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db github.com/parkervcp/fsquota v0.0.0-20240729140958-47d273ab9426 github.com/patrickmn/go-cache v2.1.0+incompatible - github.com/pkg/sftp v1.13.9 + github.com/pkg/sftp v1.13.10 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 github.com/shirou/gopsutil/v3 v3.24.5 - github.com/spf13/cobra v1.10.1 + github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 github.com/tidwall/gjson v1.18.0 - github.com/tidwall/pretty v1.2.0 + github.com/tidwall/pretty v1.2.1 github.com/tidwall/sjson v1.2.5 - golang.org/x/crypto v0.41.0 - golang.org/x/sync v0.16.0 - golang.org/x/sys v0.35.0 - gopkg.in/ini.v1 v1.67.0 + golang.org/x/crypto v0.48.0 + golang.org/x/sync v0.19.0 + golang.org/x/sys v0.41.0 + gopkg.in/ini.v1 v1.67.1 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 - gorm.io/gorm v1.30.3 + gorm.io/gorm v1.31.1 ) require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect github.com/catppuccin/go v0.3.0 // indirect - github.com/charmbracelet/bubbles v0.21.0 // indirect - github.com/charmbracelet/bubbletea v1.3.4 // indirect - github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/charmbracelet/bubbles v1.0.0 // indirect + github.com/charmbracelet/bubbletea v1.3.10 // indirect + github.com/charmbracelet/colorprofile v0.4.2 // indirect github.com/charmbracelet/lipgloss v1.1.0 // indirect - github.com/charmbracelet/x/ansi v0.8.0 // indirect - github.com/charmbracelet/x/cellbuf v0.0.13 // indirect - github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect - github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/charmbracelet/x/ansi v0.11.6 // indirect + github.com/charmbracelet/x/cellbuf v0.0.15 // indirect + github.com/charmbracelet/x/exp/strings v0.1.0 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/stringish v0.1.1 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/hashicorp/go-version v1.7.0 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect + github.com/hashicorp/go-version v1.8.0 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-runewidth v0.0.20 // indirect github.com/mikelolasagasti/xz v1.0.1 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect - github.com/moby/sys/mountinfo v0.7.1 // indirect + github.com/moby/sys/mountinfo v0.7.2 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/tidwall/match v1.1.1 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/tidwall/match v1.2.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect ) require ( - github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/Microsoft/hcsshim v0.12.2 // indirect - github.com/STARRY-S/zip v0.2.1 // indirect - github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/Microsoft/hcsshim v0.13.0 // indirect + github.com/STARRY-S/zip v0.2.3 // indirect + github.com/andybalholm/brotli v1.2.0 // indirect github.com/bodgit/plumbing v1.3.0 // indirect - github.com/bodgit/sevenzip v1.6.0 // indirect + github.com/bodgit/sevenzip v1.6.1 // indirect github.com/bodgit/windows v1.0.1 // indirect - github.com/bytedance/sonic v1.11.6 // indirect - github.com/bytedance/sonic/loader v0.1.1 // indirect - github.com/charmbracelet/huh v0.7.0 - github.com/cloudwego/base64x v0.1.4 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/charmbracelet/huh v0.8.0 + github.com/cloudwego/base64x v0.1.6 // indirect github.com/cloudwego/iasm v0.2.0 // indirect - github.com/containerd/errdefs v0.3.0 // indirect + github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect @@ -104,15 +114,15 @@ require ( github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/gammazero/deque v0.2.1 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gammazero/deque v1.2.1 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect github.com/glebarez/go-sqlite v1.22.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect @@ -121,61 +131,61 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/kr/fs v0.1.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect github.com/magefile/mage v1.15.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/minio/minlz v1.0.0 // indirect + github.com/minio/minlz v1.0.1 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/sys/atomicwriter v0.1.0 // indirect github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/morikuni/aec v1.0.0 // indirect - github.com/ncruces/go-strftime v0.1.9 // indirect - github.com/nwaples/rardecode/v2 v2.1.0 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/nwaples/rardecode/v2 v2.2.2 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 - github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 + github.com/pierrec/lz4/v4 v4.1.25 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/robfig/cron/v3 v3.0.1 // indirect - github.com/shoenig/go-m1cpu v0.1.6 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/sorairolake/lzip-go v0.3.5 // indirect - github.com/spf13/pflag v1.0.9 // indirect - github.com/tklauser/go-sysconf v0.3.12 // indirect - github.com/tklauser/numcpus v0.6.1 // indirect + github.com/shoenig/go-m1cpu v0.1.7 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect + github.com/sorairolake/lzip-go v0.3.8 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.12 // indirect - github.com/ulikunitz/xz v0.5.12 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect + github.com/ulikunitz/xz v0.5.15 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0 // indirect - go.opentelemetry.io/otel v1.34.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect - go.opentelemetry.io/otel/metric v1.34.0 // indirect - go.opentelemetry.io/otel/sdk v1.24.0 // indirect - go.opentelemetry.io/otel/trace v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/sdk v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go4.org v0.0.0-20230225012048-214862532bf5 // indirect - golang.org/x/arch v0.8.0 // indirect - golang.org/x/mod v0.26.0 // indirect - golang.org/x/net v0.42.0 // indirect - golang.org/x/text v0.28.0 // indirect - golang.org/x/time v0.0.0-20220922220347-f3bd1da661af - golang.org/x/tools v0.35.0 // indirect - golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect - google.golang.org/protobuf v1.34.2 // indirect + go4.org v0.0.0-20260112195520-a5071408f32f // indirect + golang.org/x/arch v0.24.0 // indirect + golang.org/x/mod v0.33.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/time v0.14.0 + golang.org/x/tools v0.42.0 // indirect + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/protobuf v1.36.11 // indirect gotest.tools/v3 v3.0.2 // indirect - modernc.org/libc v1.49.3 // indirect - modernc.org/mathutil v1.6.0 // indirect - modernc.org/memory v1.8.0 // indirect - modernc.org/sqlite v1.29.6 // indirect + modernc.org/libc v1.69.0 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.46.1 // indirect ) diff --git a/go.sum b/go.sum index 1cc9f82f..aa85082a 100644 --- a/go.sum +++ b/go.sum @@ -25,16 +25,24 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/hcsshim v0.12.2 h1:AcXy+yfRvrx20g9v7qYaJv5Rh+8GaHOS6b8G6Wx/nKs= github.com/Microsoft/hcsshim v0.12.2/go.mod h1:RZV12pcHCXQ42XnlQ3pz6FZfmrC1C+R4gaOHhRNML1g= +github.com/Microsoft/hcsshim v0.13.0 h1:/BcXOiS6Qi7N9XqUcv27vkIuVOkBEcWstd2pMlWSeaA= +github.com/Microsoft/hcsshim v0.13.0/go.mod h1:9KWJ/8DgU+QzYGupX4tzMhRQE8h6w90lH6HAaclpEok= github.com/NYTimes/logrotate v1.0.0 h1:6jFGbon6jOtpy3t3kwZZKS4Gdmf1C/Wv5J4ll4Xn5yk= github.com/NYTimes/logrotate v1.0.0/go.mod h1:GxNz1cSw1c6t99PXoZlw+nm90H6cyQyrH66pjVv7x88= github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg= github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4= +github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4= +github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk= github.com/acobaugh/osrelease v0.1.0 h1:Yb59HQDGGNhCj4suHaFQQfBps5wyoKLSSX/J/+UifRE= github.com/acobaugh/osrelease v0.1.0/go.mod h1:4bFEs0MtgHNHBrmHCt67gNisnabCRAlzdVasCEGHTWY= github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 h1:8PmGpDEZl9yDpcdEr6Odf23feCxK3LNUNMxjXg41pZQ= github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/apex/log v1.9.0 h1:FHtw/xuaM8AgmvDDTI9fiwoAL25Sq2cxojnZICUU8l0= github.com/apex/log v1.9.0/go.mod h1:m82fZlWIuiWzWP04XCTXmnX0xRkYYbCdYn8jbJeLBEA= github.com/apex/logs v1.0.0/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo= @@ -50,39 +58,62 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= github.com/beevik/etree v1.6.0 h1:u8Kwy8pp9D9XeITj2Z0XtA5qqZEmtJtuXZRQi+j03eE= github.com/beevik/etree v1.6.0/go.mod h1:bh4zJxiIr62SOf9pRzN7UUYaEDa9HEKafK25+sLc0Gc= github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs= github.com/bodgit/sevenzip v1.6.0 h1:a4R0Wu6/P1o1pP/3VV++aEOcyeBxeO/xE2Y9NSTrr6A= github.com/bodgit/sevenzip v1.6.0/go.mod h1:zOBh9nJUof7tcrlqJFv1koWRrhz3LbDbUNngkuZxLMc= +github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4= +github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8= github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= +github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc= +github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI= github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= +github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8= github.com/charmbracelet/huh v0.7.0 h1:W8S1uyGETgj9Tuda3/JdVkc3x7DBLZYPZc4c+/rnRdc= github.com/charmbracelet/huh v0.7.0/go.mod h1:UGC3DZHlgOKHvHC07a5vHag41zzhpPFj34U92sOmyuk= +github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY= +github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= +github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= +github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U= github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ= github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= @@ -91,8 +122,12 @@ github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payR github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= +github.com/charmbracelet/x/exp/strings v0.1.0 h1:i69S2XI7uG1u4NLGeJPSYU++Nmjvpo9nwd6aoEm7gkA= +github.com/charmbracelet/x/exp/strings v0.1.0/go.mod h1:/ehtMPNh9K4odGFkqYJKpIYyePhdp1hLBRvyY4bWkH8= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI= @@ -101,12 +136,22 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4= github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= @@ -125,6 +170,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM= github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= +github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -150,32 +197,48 @@ github.com/g0rbe/go-chattr v1.0.1 h1:CHwYB+WKB46hkzt6Jxyvkyrz7u9njghUOFvmx2gir84 github.com/g0rbe/go-chattr v1.0.1/go.mod h1:yQc6VPJfpDDC1g+W2t47+yYmzBNioax/GLiyJ25/IOs= github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gammazero/deque v0.2.1 h1:qSdsbG6pgp6nL7A0+K/B7s12mcCY/5l5SIUpMOl+dC0= github.com/gammazero/deque v0.2.1/go.mod h1:LFroj8x4cMYCukHJDbxFCkT+r9AndaJnFMuZDV34tuU= +github.com/gammazero/deque v1.2.1 h1:9fnQVFCCZ9/NOc7ccTNqzoKd1tCWOqeI05/lPqFPMGQ= +github.com/gammazero/deque v1.2.1/go.mod h1:5nSFkzVm+afG9+gy0VIowlqVAW4N8zNcMne+CMQVD2g= github.com/gammazero/workerpool v1.1.3 h1:WixN4xzukFoN0XSeXF6puqEqFTl2mECI9S6W44HWy9Q= github.com/gammazero/workerpool v1.1.3/go.mod h1:wPjyBLDbyKnUn2XwwyD3EEwo9dHutia9/fwNmSHWACc= +github.com/gammazero/workerpool v1.2.1 h1:MEDvUJsNYGuCvl1RwIXNKu2YtQtHqCSF9XWF04N7lqs= +github.com/gammazero/workerpool v1.2.1/go.mod h1:E32GVRUanF4d6QtRmdss3AScgaDkIyrvPtgRQUWgmx4= github.com/gbrlsnchs/jwt/v3 v3.0.1 h1:lbUmgAKpxnClrKloyIwpxm4OuWeDl5wLk52G91ODPw4= github.com/gbrlsnchs/jwt/v3 v3.0.1/go.mod h1:AncDcjXz18xetI3A6STfXq2w+LuTx8pQ8bGEwRN8zVM= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= +github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/go-co-op/gocron/v2 v2.16.5 h1:j228Jxk7bb9CF8LKR3gS+bK3rcjRUINjlVI+ZMp26Ss= github.com/go-co-op/gocron/v2 v2.16.5/go.mod h1:zAfC/GFQ668qHxOVl/D68Jh5Ce7sDqX6TJnSQyRkRBc= +github.com/go-co-op/gocron/v2 v2.19.1 h1:B4iLeA0NB/2iO3EKQ7NfKn5KsQgZfjb2fkvoZJU3yBI= +github.com/go-co-op/gocron/v2 v2.19.1/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -184,8 +247,12 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -208,6 +275,7 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -215,6 +283,7 @@ github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OI github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -232,6 +301,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= +github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= @@ -262,10 +333,14 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= @@ -284,8 +359,12 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM= +github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/magefile/mage v1.9.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= @@ -303,13 +382,19 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ= +github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mholt/archives v0.1.3 h1:aEAaOtNra78G+TvV5ohmXrJOAzf++dIlYeDW3N9q458= github.com/mholt/archives v0.1.3/go.mod h1:LUCGp++/IbV/I0Xq4SzcIR6uwgeh2yjnQWamjRQfLTU= +github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ= +github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4= github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0= github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc= github.com/minio/minlz v1.0.0 h1:Kj7aJZ1//LlTP1DM8Jm7lNKvvJS2m74gyyXXn3+uJWQ= github.com/minio/minlz v1.0.0/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= +github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A= +github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= @@ -320,6 +405,8 @@ github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/moby/sys/mountinfo v0.7.1 h1:/tTvQaSJRr2FshkhXiIpux6fQ2Zvc4j7tAhMTStAG2g= github.com/moby/sys/mountinfo v0.7.1/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= +github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae h1:O4SWKdcHVCvYqyDV+9CJA1fcDN2L11Bule0iFy3YlAI= @@ -339,33 +426,52 @@ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nwaples/rardecode/v2 v2.1.0 h1:JQl9ZoBPDy+nIZGb1mx8+anfHp/LV3NE2MjMiv0ct/U= github.com/nwaples/rardecode/v2 v2.1.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= +github.com/nwaples/rardecode/v2 v2.2.2 h1:/5oL8dzYivRM/tqX9VcTSWfbpwcbwKG1QtSJr3b3KcU= +github.com/nwaples/rardecode/v2 v2.2.2/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/parkervcp/fsquota v0.0.0-20240729140958-47d273ab9426 h1:4JbdZ5mDEEz1FkH48AZsn68MGgcEwzJbYUyXeRErGk0= github.com/parkervcp/fsquota v0.0.0-20240729140958-47d273ab9426/go.mod h1:8T5p+Jn1ZK/bz/F+QBxaMrzjpi3/lCjISQjUdVSW+DQ= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0= +github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.9 h1:4NGkvGudBL7GteO3m6qnaQ4pC0Kvf0onSVc9gR3EWBw= github.com/pkg/sftp v1.13.9/go.mod h1:OBN7bVXdstkFFN/gdnHPUb5TE8eb8G1Rp9wCItqjkkA= +github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU= +github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA= 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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -377,6 +483,7 @@ github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI= @@ -386,20 +493,33 @@ github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/i github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/go-m1cpu v0.1.7 h1:C76Yd0ObKR82W4vhfjZiCp0HxcSZ8Nqd84v+HZ0qyI0= +github.com/shoenig/go-m1cpu v0.1.7/go.mod h1:KkDOw6m3ZJQAPHbrzkZki4hnx+pDRR1Lo+ldA56wD5w= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= github.com/sorairolake/lzip-go v0.3.5 h1:ms5Xri9o1JBIWvOFAorYtUNik6HI3HgBTkISiqu0Cwg= github.com/sorairolake/lzip-go v0.3.5/go.mod h1:N0KYq5iWrMXI0ZEXKXaS9hCyOjZUQdBDEIbXfoUwbdk= +github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik= +github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -413,6 +533,7 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -420,8 +541,12 @@ github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= +github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= @@ -433,15 +558,23 @@ github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPf github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= +github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= @@ -449,26 +582,40 @@ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3i github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0 h1:cEPbyTSEHlQR89XVlyo78gqluF8Y3oMeBkXGWzQsfXY= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0/go.mod h1:DKdbWcT4GH1D0Y3Sqt/PFXt2naRKDWtU+eE6oLdFNA8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM= go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -477,11 +624,16 @@ go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU= +go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw= +go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y= +golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -495,6 +647,8 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -505,6 +659,7 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -529,6 +684,8 @@ golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -553,6 +710,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -572,6 +731,8 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -595,6 +756,7 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -605,6 +767,8 @@ golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -616,6 +780,7 @@ golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -630,10 +795,14 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220922220347-f3bd1da661af h1:Yx9k8YCG3dvF87UAn2tu2HQLf2dt/eR1bXxpLMWeH+Y= golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -666,11 +835,15 @@ golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58 golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -698,10 +871,14 @@ google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvx google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ= +google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934= google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8= google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -711,8 +888,11 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -722,6 +902,8 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k= +gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -733,6 +915,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/gorm v1.30.3 h1:QiG8upl0Sg9ba2Zatfjy0fy4It2iNBL2/eMdvEkdXNs= gorm.io/gorm v1.30.3/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -742,26 +926,41 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= modernc.org/cc/v4 v4.20.0 h1:45Or8mQfbUqJOG9WaxvlFYOAQO0lQ5RvqBcFCXngjxk= modernc.org/cc/v4 v4.20.0/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= modernc.org/ccgo/v4 v4.16.0 h1:ofwORa6vx2FMm0916/CkZjpFPSR70VwTjUCe2Eg5BnA= modernc.org/ccgo/v4 v4.16.0/go.mod h1:dkNyWIjFrVIZ68DTo36vHK+6/ShBn4ysU61So6PIqCI= +modernc.org/ccgo/v4 v4.31.0 h1:/bsaxqdgX3gy/0DboxcvWrc3NpzH+6wpFfI/ZaA/hrg= modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/libc v1.49.3 h1:j2MRCRdwJI2ls/sGbeSk0t2bypOG/uvPZUsGQFDulqg= modernc.org/libc v1.49.3/go.mod h1:yMZuGkn7pXbKfoT/M35gFJOAEdSKdxL0q64sF7KqCDo= +modernc.org/libc v1.69.0 h1:YQJ5QMSReTgQ3QFmI0dudfjXIjCcYTUxcH8/9P9f0D8= +modernc.org/libc v1.69.0/go.mod h1:YfLLduUEbodNV2xLU5JOnRHBTAHVHsVW3bVYGw0ZCV4= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sqlite v1.29.6 h1:0lOXGrycJPptfHDuohfYgNqoe4hu+gYuN/pKgY5XjS4= modernc.org/sqlite v1.29.6/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U= +modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU= +modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= From f55b2a8195a6b5c61087db59158efd83813df0b1 Mon Sep 17 00:00:00 2001 From: "Michael (Parker) Parker" Date: Mon, 2 Mar 2026 09:40:03 -0500 Subject: [PATCH 14/15] update fsxattr to match the header docs. adds coxextsize and limits fsxpad to 8bytes corrects some errors on filesystem type detection handles when limits are lower than 0. Treats them as unlimited. now locks exfs projects when working with them to avoid any issues --- server/filesystem/quotas/exfs.go | 36 ++++++++++++++--------- server/filesystem/quotas/functions.go | 8 +++-- server/filesystem/quotas/syscall_xattr.go | 20 +++++++++---- 3 files changed, 43 insertions(+), 21 deletions(-) diff --git a/server/filesystem/quotas/exfs.go b/server/filesystem/quotas/exfs.go index 4700c184..4a0931d1 100644 --- a/server/filesystem/quotas/exfs.go +++ b/server/filesystem/quotas/exfs.go @@ -5,6 +5,7 @@ import ( "html/template" "os" "strings" + "sync" "emperror.dev/errors" "github.com/apex/log" @@ -12,7 +13,10 @@ import ( "github.com/pelican-dev/wings/config" ) -var exfsProjects []exfsProject +var exfs struct { + projects []exfsProject + lock sync.Mutex +} type exfsProject struct { ID int @@ -91,9 +95,9 @@ func (q exfsProject) addProject() (err error) { if strings.HasSuffix(q.BasePath, "/") { q.BasePath = strings.TrimSuffix(config.Get().System.Data, "/") } - - exfsProjects = append(exfsProjects, q) - + exfs.lock.Lock() + exfs.projects = append(exfs.projects, q) + exfs.lock.Unlock() if err = writeEXFSProjects(); err != nil { return } @@ -107,9 +111,12 @@ func (q exfsProject) addProject() (err error) { // removeProject drops a specified project from the func (q exfsProject) removeProject() (err error) { - for pos, project := range exfsProjects { + exfs.lock.Lock() + defer exfs.lock.Unlock() + for pos, project := range exfs.projects { if project.Name == q.Name { - exfsProjects = append(exfsProjects[:pos], exfsProjects[pos+1:]...) + exfs.projects = append(exfs.projects[:pos], exfs.projects[pos+1:]...) + break } } @@ -118,7 +125,9 @@ func (q exfsProject) removeProject() (err error) { } func getProject(serverUUID string) (serverProject exfsProject, err error) { - for _, project := range exfsProjects { + exfs.lock.Lock() + defer exfs.lock.Unlock() + for _, project := range exfs.projects { if project.Name == serverUUID { return project, nil } @@ -128,13 +137,15 @@ func getProject(serverUUID string) (serverProject exfsProject, err error) { } func writeEXFSProjects() (err error) { + exfs.lock.Lock() + defer exfs.lock.Unlock() // write out projid file idtmpl, err := template.New("projid").Parse(projidTemplate) if err != nil { return err } - if err = writeTemplate(idtmpl, projidFile, exfsProjects); err != nil { + if err = writeTemplate(idtmpl, projidFile, exfs.projects); err != nil { return } @@ -143,7 +154,7 @@ func writeEXFSProjects() (err error) { return err } - if err = writeTemplate(projtmpl, projectFile, exfsProjects); err != nil { + if err = writeTemplate(projtmpl, projectFile, exfs.projects); err != nil { return } @@ -156,12 +167,9 @@ func writeTemplate(t *template.Template, file string, data interface{}) (err err return err } - err = t.Execute(f, data) - if err != nil { - return err - } + defer f.Close() - err = f.Close() + err = t.Execute(f, data) if err != nil { return err } diff --git a/server/filesystem/quotas/functions.go b/server/filesystem/quotas/functions.go index 30ba015b..47e2ebb0 100644 --- a/server/filesystem/quotas/functions.go +++ b/server/filesystem/quotas/functions.go @@ -34,7 +34,7 @@ func getFSType(mount string) (err error) { switch fsType { case FSBTRFS: - log.WithField("fs-type", "brtfs").Debug("found filesystem") + log.WithField("fs-type", "btrfs").Debug("found filesystem") return nil case FSEXT4: log.WithField("fs-type", "ext4").Debug("found filesystem") @@ -75,7 +75,7 @@ func IsSupportedFS() (supported bool) { } else if fsType == FSBTRFS { log.WithField("path", config.Get().System.Data).Error("btrfs is not supported") } else if fsType == FSZFS { - log.WithField("path", config.Get().System.Data).Error("btrfs is not supported") + log.WithField("path", config.Get().System.Data).Error("zfs is not supported") } return @@ -106,6 +106,10 @@ func DelQuota(serverUUID string) (err error) { // SetQuota configures quotas for a specified server func SetQuota(limit int64, serverUUID string) (err error) { log.WithField("server", serverUUID).Debug("setting quota") + if limit < 0 { + limit = 0 + log.WithFields(log.Fields{"requested_limit": limit}).Error("quota limit cannot be negative, setting to zero") + } if fsType == FSEXT4 || fsType == FSXFS { fsProject, err := getProject(serverUUID) if err != nil { diff --git a/server/filesystem/quotas/syscall_xattr.go b/server/filesystem/quotas/syscall_xattr.go index 19f2fe1f..dd1c9068 100644 --- a/server/filesystem/quotas/syscall_xattr.go +++ b/server/filesystem/quotas/syscall_xattr.go @@ -44,14 +44,24 @@ const ( FS_IOC_FSSETXATTR uintptr = 0x401c5820 // https://docs.rs/linux-raw-sys/latest/linux_raw_sys/ioctl/constant.FS_IOC_FSSETXATTR.html ) +//struct fsxattr { +//__u32 fsx_xflags; /* xflags field value (get/set) */ +//__u32 fsx_extsize; /* extsize field value (get/set)*/ +//__u32 fsx_nextents; /* nextents field value (get) */ +//__u32 fsx_projid; /* project identifier (get/set) */ +//__u32 fsx_cowextsize; /* CoW extsize field value (get/set)*/ +//unsigned char fsx_pad[8]; +//}; + // fsXAttr is the struct defining the structure // for FS_IOC_FSGETXATTR and FS_IOC_FSSETXATTR type fsXAttr struct { - XFlags uint32 - ExtSize uint32 - NextENTs uint32 - ProjectID uint32 - FSXPad byte + XFlags uint32 + ExtSize uint32 + NextENTs uint32 + ProjectID uint32 + CowExtSize uint32 + FSXPad [8]byte } // xAttrCtl sets the From ca09896d5e18d93b56eee4e6a46c36f7a2b10a26 Mon Sep 17 00:00:00 2001 From: "Michael (Parker) Parker" Date: Mon, 2 Mar 2026 10:13:32 -0500 Subject: [PATCH 15/15] better logging reorganize handling limits being below 0 handle if the project id is outside the range --- server/filesystem/quotas/exfs.go | 11 +++++++---- server/filesystem/quotas/functions.go | 5 +++-- server/filesystem/quotas/syscall_xattr.go | 9 +++++++-- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/server/filesystem/quotas/exfs.go b/server/filesystem/quotas/exfs.go index 4a0931d1..4167ba6e 100644 --- a/server/filesystem/quotas/exfs.go +++ b/server/filesystem/quotas/exfs.go @@ -36,7 +36,7 @@ const ( // setQuota sets the quota in bytes for the specified server uuid func (q exfsProject) setQuota(byteLimit uint64) (err error) { - log.WithFields(log.Fields{"server-path": fmt.Sprintf("%s/%s", q.BasePath, q.Name), "bytes": byteLimit}).Debug("setting quota") + log.WithFields(log.Fields{"server_path": fmt.Sprintf("%s/%s", q.BasePath, q.Name), "limit_bytes": byteLimit}).Debug("setting quota") serverProject, err := fsquota.LookupProject(q.Name) if err != nil { return @@ -51,7 +51,7 @@ func (q exfsProject) setQuota(byteLimit uint64) (err error) { return } - log.WithFields(log.Fields{"server-path": fmt.Sprintf("%s/%s", q.BasePath, q.Name)}).Debug("quota set") + log.WithField("server_path", fmt.Sprintf("%s/%s", q.BasePath, q.Name)).Debug("quota set") return } @@ -83,7 +83,7 @@ func (q exfsProject) enableEXFSQuota() (err error) { // enable project quota inheritance and set project id if err = setXAttr(serverDir, q.ID, FS_XFLAG_PROJINHERIT); err != nil { - log.WithFields(log.Fields{"server-uuid": q.Name, "server-path": serverDir.Name()}).Error("failed to update XATTRs for server") + log.WithFields(log.Fields{"server_uuid": q.Name, "server_path": serverDir.Name()}).Error("failed to update XATTRs for server") return } @@ -96,13 +96,16 @@ func (q exfsProject) addProject() (err error) { q.BasePath = strings.TrimSuffix(config.Get().System.Data, "/") } exfs.lock.Lock() + defer exfs.lock.Unlock() exfs.projects = append(exfs.projects, q) - exfs.lock.Unlock() + if err = writeEXFSProjects(); err != nil { + log.WithError(err).Error("failed to write exfs projects") return } if err = q.enableEXFSQuota(); err != nil { + log.WithError(err).Error("failed to enable quota") return } diff --git a/server/filesystem/quotas/functions.go b/server/filesystem/quotas/functions.go index 47e2ebb0..f1279b7a 100644 --- a/server/filesystem/quotas/functions.go +++ b/server/filesystem/quotas/functions.go @@ -56,7 +56,7 @@ func IsSupportedFS() (supported bool) { log.WithField("path", config.Get().System.Data).Debug("checking filesystem type") err := getFSType(config.Get().System.Data) if err != nil { - log.Error(err.Error()) + log.WithError(err).Error("error checking filesystem type") return } @@ -64,6 +64,7 @@ func IsSupportedFS() (supported bool) { // technically tested on EXT4 and will need to be validated for XFS supported, err = fsquota.ProjectQuotasSupported(config.Get().System.Data) if err != nil { + log.WithError(err).Error("error checking for quota support") return } @@ -107,8 +108,8 @@ func DelQuota(serverUUID string) (err error) { func SetQuota(limit int64, serverUUID string) (err error) { log.WithField("server", serverUUID).Debug("setting quota") if limit < 0 { + log.WithField("requested_limit", limit).Error("quota limit cannot be negative, setting to zero") limit = 0 - log.WithFields(log.Fields{"requested_limit": limit}).Error("quota limit cannot be negative, setting to zero") } if fsType == FSEXT4 || fsType == FSXFS { fsProject, err := getProject(serverUUID) diff --git a/server/filesystem/quotas/syscall_xattr.go b/server/filesystem/quotas/syscall_xattr.go index dd1c9068..fd4c11d9 100644 --- a/server/filesystem/quotas/syscall_xattr.go +++ b/server/filesystem/quotas/syscall_xattr.go @@ -4,6 +4,7 @@ import ( "os" "unsafe" + "emperror.dev/errors" "golang.org/x/sys/unix" ) @@ -64,7 +65,7 @@ type fsXAttr struct { FSXPad [8]byte } -// xAttrCtl sets the +// xAttrCtl handles the xattr calls for a specified file func xAttrCtl(f *os.File, request uintptr, xattr *fsXAttr) (err error) { xattreq := uintptr(unsafe.Pointer(xattr)) @@ -85,8 +86,12 @@ func getXAttr(f *os.File) (attr fsXAttr, err error) { return } -// setXAttr sets xattr values for the +// setXAttr sets xattr values for a specified file func setXAttr(f *os.File, projectID int, attr uint32) (err error) { + if projectID < 0 || uint64(projectID) > uint64(^uint32(0)) { + return errors.New("projectID out of range") + } + fxattr, err := getXAttr(f) if err != nil { return err