Compare commits
15 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
b00d328107 | ||
|
daa0ab75b4 | ||
|
ff4b7655c8 | ||
|
99cb61a6ef | ||
|
0407f22147 | ||
|
a74ea6a9ff | ||
|
37c52dd439 | ||
|
f8a25cb040 | ||
|
a6a610fd82 | ||
|
dfe5a77e0a | ||
|
d8a7bf2dde | ||
|
265f8a6b39 | ||
|
7fed6a68cb | ||
|
b0f99e2328 | ||
|
957257ecc3 |
3
Makefile
3
Makefile
|
@ -4,6 +4,9 @@ build:
|
|||
GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -gcflags "all=-trimpath=$(pwd)" -o build/wings_linux_amd64 -v wings.go
|
||||
GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -gcflags "all=-trimpath=$(pwd)" -o build/wings_linux_arm64 -v wings.go
|
||||
|
||||
race:
|
||||
go build -ldflags="-X github.com/pterodactyl/wings/system.Version=$(GIT_HEAD)" -race
|
||||
|
||||
debug:
|
||||
go build -ldflags="-X github.com/pterodactyl/wings/system.Version=$(GIT_HEAD)"
|
||||
sudo ./wings --debug --ignore-certificate-errors --config config.yml --pprof --pprof-block-rate 1
|
||||
|
|
127
cmd/migrate_vhd.go
Normal file
127
cmd/migrate_vhd.go
Normal file
|
@ -0,0 +1,127 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"emperror.dev/errors"
|
||||
"github.com/apex/log"
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/pterodactyl/wings/internal/vhd"
|
||||
"github.com/pterodactyl/wings/loggers/cli"
|
||||
"github.com/pterodactyl/wings/remote"
|
||||
"github.com/pterodactyl/wings/server"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type MigrateVHDCommand struct {
|
||||
manager *server.Manager
|
||||
}
|
||||
|
||||
func newMigrateVHDCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "migrate-vhd",
|
||||
Short: "migrates existing data from a directory tree into virtual hard-disks",
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
log.SetHandler(cli.Default)
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
client := remote.NewFromConfig(config.Get())
|
||||
manager, err := server.NewManager(cmd.Context(), client, true)
|
||||
if err != nil {
|
||||
log.WithField("error", err).Fatal("failed to create new server manager")
|
||||
}
|
||||
c := &MigrateVHDCommand{
|
||||
manager: manager,
|
||||
}
|
||||
if err := c.Run(cmd.Context()); err != nil {
|
||||
log.WithField("error", err).Fatal("failed to execute command")
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Run executes the migration command.
|
||||
func (m *MigrateVHDCommand) Run(ctx context.Context) error {
|
||||
if !vhd.Enabled() {
|
||||
return errors.New("cannot migrate to vhd: the underlying driver must be set to \"vhd\"")
|
||||
}
|
||||
for _, s := range m.manager.All() {
|
||||
s.Log().Debug("starting migration of server contents to virtual disk...")
|
||||
|
||||
v := vhd.New(s.DiskSpace(), vhd.DiskPath(s.ID()), s.Filesystem().Path())
|
||||
s.Log().WithField("disk_image", v.Path()).Info("creating virtual disk for server")
|
||||
if err := v.Allocate(ctx); err != nil {
|
||||
return errors.WithStackIf(err)
|
||||
}
|
||||
|
||||
s.Log().Info("creating virtual filesystem for server")
|
||||
if err := v.MakeFilesystem(ctx); err != nil {
|
||||
// If the filesystem already exists no worries, just move on with our
|
||||
// day here.
|
||||
if !errors.Is(err, vhd.ErrFilesystemExists) {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
bak := strings.TrimSuffix(s.Filesystem().Path(), "/") + "_bak"
|
||||
mounted, err := v.IsMounted(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !mounted {
|
||||
s.Log().WithField("backup_dir", bak).Debug("virtual disk is not yet mounted, creating backup directory")
|
||||
// Create a backup directory of the server files if one does not already exist
|
||||
// at that location. If one does exists we'll just assume it is good to go and
|
||||
// rely on it to provide the files we'll need.
|
||||
if _, err := os.Lstat(bak); os.IsNotExist(err) {
|
||||
if err := os.Rename(s.Filesystem().Path(), bak); err != nil {
|
||||
return errors.Wrap(err, "failed to rename existing data directory for backup")
|
||||
}
|
||||
} else if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := os.RemoveAll(s.Filesystem().Path()); err != nil && !os.IsNotExist(err) {
|
||||
return errors.Wrap(err, "failed to remove base server files path")
|
||||
}
|
||||
} else {
|
||||
s.Log().Warn("server appears to already have existing mount, not creating data backup")
|
||||
}
|
||||
|
||||
// Attempt to mount the disk at the expected path now that we've created
|
||||
// a backup of the server files.
|
||||
if err := v.Mount(ctx); err != nil && !errors.Is(err, vhd.ErrFilesystemMounted) {
|
||||
return errors.WithStackIf(err)
|
||||
}
|
||||
|
||||
// Copy over the files from the backup for this server but only
|
||||
// if we have a backup directory currently.
|
||||
_, err = os.Lstat(bak)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
s.Log().WithField("error", err).Warn("failed to stat backup directory")
|
||||
} else {
|
||||
s.Log().Info("no backup data directory exists, not restoring files")
|
||||
}
|
||||
} else {
|
||||
cmd := exec.CommandContext(ctx, "cp", "-r", bak+"/.", s.Filesystem().Path())
|
||||
if err := cmd.Run(); err != nil {
|
||||
return errors.Wrap(err, "migrate: failed to move old server files into new direcotry")
|
||||
} else {
|
||||
if err := os.RemoveAll(bak); err != nil {
|
||||
s.Log().WithField("directory", bak).WithField("error", err).Warn("failed to remove backup directory")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.Log().Info("updating server file ownership...")
|
||||
if err := s.Filesystem().Chown("/"); err != nil {
|
||||
s.Log().WithField("error", err).Warn("failed to update ownership of new server files")
|
||||
}
|
||||
|
||||
s.Log().Info("finished migration to virtual disk...")
|
||||
}
|
||||
return nil
|
||||
}
|
22
cmd/root.go
22
cmd/root.go
|
@ -47,8 +47,16 @@ var (
|
|||
var rootCommand = &cobra.Command{
|
||||
Use: "wings",
|
||||
Short: "Runs the API server allowing programmatic control of game servers for Pterodactyl Panel.",
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
initConfig()
|
||||
if ok, _ := cmd.Flags().GetBool("ignore-certificate-errors"); ok {
|
||||
log.Warn("running with --ignore-certificate-errors: TLS certificate host chains and name will not be verified")
|
||||
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
initLogging()
|
||||
if tls, _ := cmd.Flags().GetBool("auto-tls"); tls {
|
||||
if host, _ := cmd.Flags().GetString("tls-hostname"); host == "" {
|
||||
|
@ -77,6 +85,7 @@ func Execute() {
|
|||
func init() {
|
||||
rootCommand.PersistentFlags().StringVar(&configPath, "config", config.DefaultLocation, "set the location for the configuration file")
|
||||
rootCommand.PersistentFlags().BoolVar(&debug, "debug", false, "pass in order to run wings in debug mode")
|
||||
rootCommand.PersistentFlags().Bool("ignore-certificate-errors", false, "ignore certificate verification errors when executing API calls")
|
||||
|
||||
// Flags specifically used when running the API.
|
||||
rootCommand.Flags().Bool("pprof", false, "if the pprof profiler should be enabled. The profiler will bind to localhost:6060 by default")
|
||||
|
@ -84,11 +93,11 @@ func init() {
|
|||
rootCommand.Flags().Int("pprof-port", 6060, "If provided with --pprof, the port it will run on")
|
||||
rootCommand.Flags().Bool("auto-tls", false, "pass in order to have wings generate and manage its own SSL certificates using Let's Encrypt")
|
||||
rootCommand.Flags().String("tls-hostname", "", "required with --auto-tls, the FQDN for the generated SSL certificate")
|
||||
rootCommand.Flags().Bool("ignore-certificate-errors", false, "ignore certificate verification errors when executing API calls")
|
||||
|
||||
rootCommand.AddCommand(versionCommand)
|
||||
rootCommand.AddCommand(configureCmd)
|
||||
rootCommand.AddCommand(newDiagnosticsCommand())
|
||||
rootCommand.AddCommand(newMigrateVHDCommand())
|
||||
}
|
||||
|
||||
func rootCmdRun(cmd *cobra.Command, _ []string) {
|
||||
|
@ -96,13 +105,6 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
|
|||
log.Debug("running in debug mode")
|
||||
log.WithField("config_file", configPath).Info("loading configuration from file")
|
||||
|
||||
if ok, _ := cmd.Flags().GetBool("ignore-certificate-errors"); ok {
|
||||
log.Warn("running with --ignore-certificate-errors: TLS certificate host chains and name will not be verified")
|
||||
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
}
|
||||
|
||||
if err := config.ConfigureTimezone(); err != nil {
|
||||
log.WithField("error", err).Fatal("failed to detect system timezone or use supplied configuration value")
|
||||
}
|
||||
|
@ -137,7 +139,7 @@ func rootCmdRun(cmd *cobra.Command, _ []string) {
|
|||
log.WithField("error", err).Fatal("failed to initialize database")
|
||||
}
|
||||
|
||||
manager, err := server.NewManager(cmd.Context(), pclient)
|
||||
manager, err := server.NewManager(cmd.Context(), pclient, false)
|
||||
if err != nil {
|
||||
log.WithField("error", err).Fatal("failed to load server configurations")
|
||||
}
|
||||
|
|
|
@ -305,6 +305,11 @@ type Configuration struct {
|
|||
// is only required by users running Wings without SSL certificates and using internal IP
|
||||
// addresses in order to connect. Most users should NOT enable this setting.
|
||||
AllowCORSPrivateNetwork bool `json:"allow_cors_private_network" yaml:"allow_cors_private_network"`
|
||||
|
||||
// Servers contains all of the settings that are used when configuring individual servers
|
||||
// on the system. This is a global configuration for all server instances, not to be confused
|
||||
// with the per-server configurations provided by the Panel API.
|
||||
Servers Servers `json:"servers" yaml:"servers"`
|
||||
}
|
||||
|
||||
// NewAtPath creates a new struct and set the path where it should be stored.
|
||||
|
|
28
config/config_servers.go
Normal file
28
config/config_servers.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package config
|
||||
|
||||
type FSDriver string
|
||||
|
||||
const (
|
||||
FSDriverLocal FSDriver = "local"
|
||||
FSDriverVHD FSDriver = "vhd"
|
||||
)
|
||||
|
||||
type Servers struct {
|
||||
// Filesystem defines all of the filesystem specific settings used for servers.
|
||||
Filesystem Filesystem `json:"filesystem" yaml:"filesystem"`
|
||||
}
|
||||
|
||||
type Filesystem struct {
|
||||
// Driver defines the underlying filesystem driver that is used when a server is
|
||||
// created on the system. This currently supports either of the following drivers:
|
||||
//
|
||||
// local: the local driver is the default one used by Wings. This offloads all of the
|
||||
// disk limit enforcement to Wings itself. This has a performance impact but is
|
||||
// the most compatiable with all systems.
|
||||
// vhd: the vhd driver uses "virtual" disks on the host system to enforce disk limits
|
||||
// on the server. This is more performant since calculations do not need to be made
|
||||
// by Wings itself when enforcing limits. It also avoids vulnerabilities that exist
|
||||
// in the local driver which allow malicious processes to quickly create massive files
|
||||
// before Wings is able to detect and stop them from being written.
|
||||
Driver FSDriver `default:"local" json:"driver" yaml:"driver"`
|
||||
}
|
3
go.mod
3
go.mod
|
@ -40,6 +40,7 @@ require (
|
|||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/pkg/sftp v1.13.5
|
||||
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
|
||||
github.com/spf13/afero v1.9.2
|
||||
github.com/spf13/cobra v1.5.0
|
||||
github.com/stretchr/testify v1.8.0
|
||||
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be
|
||||
|
@ -112,7 +113,7 @@ require (
|
|||
golang.org/x/net v0.0.0-20221004154528-8021a29435af // indirect
|
||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect
|
||||
golang.org/x/term v0.0.0-20220919170432-7a66f970e087 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/text v0.3.8 // indirect
|
||||
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect
|
||||
golang.org/x/tools v0.1.12 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
|
|
57
go.sum
57
go.sum
|
@ -4,6 +4,7 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
|
|||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
|
@ -14,6 +15,9 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV
|
|||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
|
||||
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
|
||||
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
|
@ -31,6 +35,7 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
|
|||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
emperror.dev/errors v0.8.1 h1:UavXZ5cSX/4u9iyvH6aDcuGkVjeexUGJ7Ij7G4VfQT0=
|
||||
emperror.dev/errors v0.8.1/go.mod h1:YcRvLPh626Ubn2xqtoprejnA5nFha+TJ+2vew48kWuE=
|
||||
|
@ -149,6 +154,7 @@ github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJ
|
|||
github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cobaugh/osrelease v0.0.0-20181218015638-a93a0a55a249 h1:R0IDH8daQ3lODvu8YtxnIqqth5qMGCJyADoUQvmLx4o=
|
||||
|
@ -311,6 +317,7 @@ github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT
|
|||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
|
@ -452,6 +459,7 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
|
|||
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
|
@ -459,6 +467,9 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf
|
|||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
|
@ -469,6 +480,7 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
|
|||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
|
||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
|
||||
github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
|
@ -498,6 +510,7 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
|
|||
github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0=
|
||||
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/icza/dyno v0.0.0-20220812133438-f0b6f8a18845 h1:H+uM0Bv88eur3ZSsd2NGKg3YIiuXxwxtlN7HjE66UTU=
|
||||
github.com/icza/dyno v0.0.0-20220812133438-f0b6f8a18845/go.mod h1:c1tRKs5Tx7E2+uHGSyyncziFjvGpgv4H2HrqXeUQ/Uk=
|
||||
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
|
@ -703,6 +716,7 @@ github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV
|
|||
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.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||
github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go=
|
||||
github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
|
@ -790,6 +804,8 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k
|
|||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw=
|
||||
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
|
@ -879,6 +895,7 @@ 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.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
|
@ -906,6 +923,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
|||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A=
|
||||
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
|
@ -931,6 +950,7 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl
|
|||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
|
@ -939,6 +959,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
|
|||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
|
@ -980,7 +1002,9 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R
|
|||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
|
@ -997,6 +1021,10 @@ golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4Iltr
|
|||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
@ -1070,6 +1098,7 @@ golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -1080,10 +1109,14 @@ golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
@ -1113,8 +1146,9 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
|||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
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=
|
||||
|
@ -1174,9 +1208,16 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
|
|||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||
golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
|
@ -1203,12 +1244,16 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M
|
|||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
|
||||
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
|
||||
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
|
@ -1243,7 +1288,14 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc
|
|||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
|
@ -1260,8 +1312,11 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa
|
|||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
|
|
|
@ -38,7 +38,7 @@ func New(ctx context.Context, manager *server.Manager, details ServerDetails) (*
|
|||
|
||||
// Create a new server instance using the configuration we wrote to the disk
|
||||
// so that everything gets instantiated correctly on the struct.
|
||||
s, err := manager.InitServer(c)
|
||||
s, err := manager.InitServer(ctx, c)
|
||||
if err != nil {
|
||||
return nil, errors.WrapIf(err, "installer: could not init server instance")
|
||||
}
|
||||
|
|
330
internal/vhd/vhd.go
Normal file
330
internal/vhd/vhd.go
Normal file
|
@ -0,0 +1,330 @@
|
|||
package vhd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"emperror.dev/errors"
|
||||
"fmt"
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/spf13/afero"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidDiskPathTarget = errors.Sentinel("vhd: disk path is a directory or symlink")
|
||||
ErrMountPathNotDirectory = errors.Sentinel("vhd: mount point is not a directory")
|
||||
ErrFilesystemMounted = errors.Sentinel("vhd: filesystem is already mounted")
|
||||
ErrFilesystemNotMounted = errors.Sentinel("vhd: filesystem is not mounted")
|
||||
ErrFilesystemExists = errors.Sentinel("vhd: filesystem already exists on disk")
|
||||
)
|
||||
|
||||
var useDdAllocation bool
|
||||
var setDdAllocator sync.Once
|
||||
|
||||
// hasExitCode allows this code to test the response error to see if there is
|
||||
// an exit code available from the command call that can be used to determine if
|
||||
// something went wrong.
|
||||
type hasExitCode interface {
|
||||
ExitCode() int
|
||||
}
|
||||
|
||||
// Commander defines an interface that must be met for executing commands on the
|
||||
// underlying OS. By default the vhd package will use Go's exec.Cmd type for
|
||||
// execution. This interface allows stubbing out on tests, or potentially custom
|
||||
// setups down the line.
|
||||
type Commander interface {
|
||||
Run() error
|
||||
Output() ([]byte, error)
|
||||
String() string
|
||||
}
|
||||
|
||||
// CommanderProvider is a function that provides a struct meeting the Commander
|
||||
// interface requirements.
|
||||
type CommanderProvider func(ctx context.Context, name string, args ...string) Commander
|
||||
|
||||
// CfgOption is a configuration option callback for the Disk.
|
||||
type CfgOption func(d *Disk) *Disk
|
||||
|
||||
// Disk represents the underlying virtual disk for the instance.
|
||||
type Disk struct {
|
||||
mu sync.RWMutex
|
||||
// The total size of the disk allowed in bytes.
|
||||
size int64
|
||||
// The path where the disk image should be created.
|
||||
diskPath string
|
||||
// The point at which this disk should be made available on the system. This
|
||||
// is where files can be read/written to.
|
||||
mountAt string
|
||||
fs afero.Fs
|
||||
commander CommanderProvider
|
||||
}
|
||||
|
||||
// DiskPath returns the underlying path that contains the virtual disk for the server
|
||||
// identified by its UUID.
|
||||
func DiskPath(uuid string) string {
|
||||
return filepath.Join(config.Get().System.Data, ".vhd/", uuid+".img")
|
||||
}
|
||||
|
||||
// Enabled returns true when VHD support is enabled on the instance.
|
||||
func Enabled() bool {
|
||||
return config.Get().Servers.Filesystem.Driver == config.FSDriverVHD
|
||||
}
|
||||
|
||||
// New returns a new Disk instance. The "size" parameter should be provided in
|
||||
// bytes of space allowed for the disk. An additional slice of option callbacks
|
||||
// can be provided to programatically swap out the underlying filesystem
|
||||
// implementation or the underlying command exection engine.
|
||||
func New(size int64, diskPath string, mountAt string, opts ...func(*Disk)) *Disk {
|
||||
if diskPath == "" || mountAt == "" {
|
||||
panic("vhd: cannot specify an empty disk or mount path")
|
||||
}
|
||||
d := Disk{
|
||||
size: size,
|
||||
diskPath: diskPath,
|
||||
mountAt: mountAt,
|
||||
fs: afero.NewOsFs(),
|
||||
commander: func(ctx context.Context, name string, args ...string) Commander {
|
||||
return exec.CommandContext(ctx, name, args...)
|
||||
},
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(&d)
|
||||
}
|
||||
return &d
|
||||
}
|
||||
|
||||
// WithFs allows for a different underlying filesystem to be provided to the
|
||||
// virtual disk manager.
|
||||
func WithFs(fs afero.Fs) func(*Disk) {
|
||||
return func(d *Disk) {
|
||||
d.fs = fs
|
||||
}
|
||||
}
|
||||
|
||||
// WithCommander allows a different Commander provider to be provided.
|
||||
func WithCommander(c CommanderProvider) func(*Disk) {
|
||||
return func(d *Disk) {
|
||||
d.commander = c
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Disk) Path() string {
|
||||
return d.diskPath
|
||||
}
|
||||
|
||||
func (d *Disk) MountPath() string {
|
||||
return d.mountAt
|
||||
}
|
||||
|
||||
// Exists reports if the disk exists on the system yet or not. This only verifies
|
||||
// the presence of the disk image, not the validity of it. An error is returned
|
||||
// if the path exists but the destination is not a file or is a symlink.
|
||||
func (d *Disk) Exists() (bool, error) {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
st, err := d.fs.Stat(d.diskPath)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
return false, nil
|
||||
} else if err != nil {
|
||||
return false, errors.WithStack(err)
|
||||
}
|
||||
if !st.IsDir() && st.Mode()&os.ModeSymlink == 0 {
|
||||
return true, nil
|
||||
}
|
||||
return false, errors.WithStack(ErrInvalidDiskPathTarget)
|
||||
}
|
||||
|
||||
// IsMounted checks to see if the given disk is currently mounted.
|
||||
func (d *Disk) IsMounted(ctx context.Context) (bool, error) {
|
||||
find := d.mountAt + " ext4"
|
||||
cmd := d.commander(ctx, "grep", "-qs", find, "/proc/mounts")
|
||||
if err := cmd.Run(); err != nil {
|
||||
if v, ok := err.(hasExitCode); ok {
|
||||
if v.ExitCode() == 1 {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
return false, errors.Wrap(err, "vhd: failed to execute grep for mount existence")
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Mount attempts to mount the disk as configured. If it does not exist or the
|
||||
// mount command fails an error will be returned to the caller. This does not
|
||||
// attempt to create the disk if it is missing from the filesystem.
|
||||
//
|
||||
// Attempting to mount a disk which does not exist will result in an error being
|
||||
// returned to the caller. If the disk is already mounted an ErrFilesystemMounted
|
||||
// error is returned to the caller.
|
||||
func (d *Disk) Mount(ctx context.Context) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
return d.mount(ctx)
|
||||
}
|
||||
|
||||
// Unmount attempts to unmount the disk from the system. If the disk is not
|
||||
// currently mounted this function is a no-op and ErrFilesystemNotMounted is
|
||||
// returned to the caller.
|
||||
func (d *Disk) Unmount(ctx context.Context) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
return d.unmount(ctx)
|
||||
}
|
||||
|
||||
// Allocate executes the "fallocate" command on the disk. This will first unmount
|
||||
// the disk from the system before attempting to actually allocate the space. If
|
||||
// this disk already exists on the machine it will be resized accordingly.
|
||||
//
|
||||
// DANGER! This will unmount the disk from the machine while performing this
|
||||
// action, use caution when calling it during normal processes.
|
||||
func (d *Disk) Allocate(ctx context.Context) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
if exists, err := d.Exists(); exists {
|
||||
// If the disk currently exists attempt to unmount the mount point before
|
||||
// allocating space.
|
||||
if err := d.Unmount(ctx); err != nil {
|
||||
return errors.WithStackIf(err)
|
||||
}
|
||||
} else if err != nil {
|
||||
return errors.Wrap(err, "vhd: failed to check for existence of root disk")
|
||||
}
|
||||
trim := path.Base(d.diskPath)
|
||||
if err := d.fs.MkdirAll(strings.TrimSuffix(d.diskPath, trim), 0700); err != nil {
|
||||
return errors.Wrap(err, "vhd: failed to create base vhd disk directory")
|
||||
}
|
||||
cmd := d.allocationCmd(ctx)
|
||||
if _, err := cmd.Output(); err != nil {
|
||||
msg := "vhd: failed to execute space allocation command"
|
||||
if v, ok := err.(*exec.ExitError); ok {
|
||||
stderr := strings.Trim(string(v.Stderr), ".\n")
|
||||
if !useDdAllocation && strings.HasSuffix(stderr, "not supported") {
|
||||
// Try again: fallocate is not supported on some filesystems so we'll fall
|
||||
// back to making use of dd for subsequent operations.
|
||||
setDdAllocator.Do(func() {
|
||||
useDdAllocation = true
|
||||
})
|
||||
return d.Allocate(ctx)
|
||||
}
|
||||
msg = msg + ": " + stderr
|
||||
}
|
||||
return errors.Wrap(err, msg)
|
||||
}
|
||||
return errors.WithStack(d.fs.Chmod(d.diskPath, 0600))
|
||||
}
|
||||
|
||||
// Resize will change the internal disk size limit and then allocate the new
|
||||
// space to the disk automatically.
|
||||
func (d *Disk) Resize(ctx context.Context, size int64) error {
|
||||
atomic.StoreInt64(&d.size, size)
|
||||
return d.Allocate(ctx)
|
||||
}
|
||||
|
||||
// Destroy removes the underlying allocated disk image and unmounts the disk.
|
||||
func (d *Disk) Destroy(ctx context.Context) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
if err := d.unmount(ctx); err != nil {
|
||||
return errors.WithStackIf(err)
|
||||
}
|
||||
return errors.WithStackIf(d.fs.RemoveAll(d.mountAt))
|
||||
}
|
||||
|
||||
// MakeFilesystem will attempt to execute the "mkfs" command against the disk on
|
||||
// the machine. If the disk has already been created this command will return an
|
||||
// ErrFilesystemExists error to the caller. You should manually unmount the disk
|
||||
// if it shouldn't be mounted at this point.
|
||||
func (d *Disk) MakeFilesystem(ctx context.Context) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
// If no error is returned when mounting DO NOT execute this command as it will
|
||||
// completely destroy the data stored at that location.
|
||||
err := d.Mount(ctx)
|
||||
if err == nil || errors.Is(err, ErrFilesystemMounted) {
|
||||
// If it wasn't already mounted try to clean up at this point and unmount
|
||||
// the disk. If this fails just ignore it for now.
|
||||
if err != nil {
|
||||
_ = d.Unmount(ctx)
|
||||
}
|
||||
return ErrFilesystemExists
|
||||
}
|
||||
if !strings.Contains(err.Error(), "can't find in /etc/fstab") && !strings.Contains(err.Error(), "exit status 32") {
|
||||
return errors.WrapIf(err, "vhd: unexpected error from mount command")
|
||||
}
|
||||
// As long as we got an error back that was because we couldn't find thedisk
|
||||
// in the /etc/fstab file we're good. Otherwise it means the disk probably exists
|
||||
// or something else went wrong.
|
||||
//
|
||||
// Because this is a destructive command and non-tty based exection of it implies
|
||||
// "-F" (force), we need to only run it when we can guarantee it doesn't already
|
||||
// exist. No vague "maybe that error is expected" allowed here.
|
||||
cmd := d.commander(ctx, "mkfs", "-t", "ext4", d.diskPath)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return errors.Wrap(err, "vhd: failed to make filesystem for disk")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Disk) mount(ctx context.Context) error {
|
||||
if isMounted, err := d.IsMounted(ctx); err != nil {
|
||||
return errors.WithStackIf(err)
|
||||
} else if isMounted {
|
||||
return ErrFilesystemMounted
|
||||
}
|
||||
|
||||
if st, err := d.fs.Stat(d.mountAt); err != nil && !os.IsNotExist(err) {
|
||||
return errors.Wrap(err, "vhd: failed to stat mount path")
|
||||
} else if os.IsNotExist(err) {
|
||||
if err := d.fs.MkdirAll(d.mountAt, 0700); err != nil {
|
||||
return errors.Wrap(err, "vhd: failed to create mount path")
|
||||
}
|
||||
} else if !st.IsDir() {
|
||||
return errors.WithStack(ErrMountPathNotDirectory)
|
||||
}
|
||||
|
||||
u := config.Get().System.User
|
||||
if err := d.fs.Chown(d.mountAt, u.Uid, u.Gid); err != nil {
|
||||
return errors.Wrap(err, "vhd: failed to chown mount point")
|
||||
}
|
||||
|
||||
cmd := d.commander(ctx, "mount", "-t", "auto", "-o", "loop", d.diskPath, d.mountAt)
|
||||
if _, err := cmd.Output(); err != nil {
|
||||
msg := "vhd: failed to mount disk"
|
||||
if v, ok := err.(*exec.ExitError); ok {
|
||||
msg = msg + ": " + strings.Trim(string(v.Stderr), ".\n")
|
||||
}
|
||||
return errors.Wrap(err, msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Disk) unmount(ctx context.Context) error {
|
||||
cmd := d.commander(ctx, "umount", d.mountAt)
|
||||
if err := cmd.Run(); err != nil {
|
||||
v, ok := err.(hasExitCode)
|
||||
if ok && v.ExitCode() == 32 {
|
||||
return ErrFilesystemNotMounted
|
||||
}
|
||||
return errors.Wrap(err, "vhd: failed to execute unmount command for disk")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// allocationCmd returns the command to allocate the disk image. This will attempt to
|
||||
// use the fallocate command if available, otherwise it will fall back to dd if the
|
||||
// fallocate command has previously failed.
|
||||
//
|
||||
// We use 1024 as the multiplier for all of the disk space logic within the application.
|
||||
// Passing "K" (/1024) is the same as "KiB" for fallocate, but is different than "KB" (/1000).
|
||||
func (d *Disk) allocationCmd(ctx context.Context) Commander {
|
||||
s := atomic.LoadInt64(&d.size) / 1024
|
||||
if useDdAllocation {
|
||||
return d.commander(ctx, "dd", "if=/dev/zero", fmt.Sprintf("of=%s", d.diskPath), fmt.Sprintf("bs=%dk", s), "count=1")
|
||||
}
|
||||
return d.commander(ctx, "fallocate", "-l", fmt.Sprintf("%dK", s), d.diskPath)
|
||||
}
|
476
internal/vhd/vhd_test.go
Normal file
476
internal/vhd/vhd_test.go
Normal file
|
@ -0,0 +1,476 @@
|
|||
package vhd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func init() {
|
||||
config.Set(&config.Configuration{
|
||||
AuthenticationToken: "token123",
|
||||
System: config.SystemConfiguration{
|
||||
User: struct {
|
||||
Uid int
|
||||
Gid int
|
||||
}{Uid: 10, Gid: 10},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
type mockCmd struct {
|
||||
run func() error
|
||||
output func() ([]byte, error)
|
||||
string func() string
|
||||
}
|
||||
|
||||
func (m *mockCmd) Run() error {
|
||||
if m.run != nil {
|
||||
return m.run()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockCmd) Output() ([]byte, error) {
|
||||
if m.output != nil {
|
||||
return m.output()
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockCmd) String() string {
|
||||
if m.string != nil {
|
||||
return m.string()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var _ Commander = (*mockCmd)(nil)
|
||||
|
||||
type mockedExitCode struct {
|
||||
code int
|
||||
}
|
||||
|
||||
func (m *mockedExitCode) ExitCode() int {
|
||||
return m.code
|
||||
}
|
||||
|
||||
func (m *mockedExitCode) Error() string {
|
||||
return fmt.Sprintf("mocked exit code: code %d", m.code)
|
||||
}
|
||||
|
||||
func newMockDisk(c CommanderProvider) *Disk {
|
||||
commander := func(ctx context.Context, name string, args ...string) Commander {
|
||||
return &mockCmd{}
|
||||
}
|
||||
w := commander
|
||||
if c != nil {
|
||||
w = c
|
||||
}
|
||||
return New(100 * 1024 * 1024, "/disk.img", "/mnt", WithFs(afero.NewMemMapFs()), WithCommander(w))
|
||||
}
|
||||
|
||||
func Test_New(t *testing.T) {
|
||||
t.Run("creates expected struct", func(t *testing.T) {
|
||||
d := New(100 * 1024 * 1024, "/disk.img", "/mnt")
|
||||
assert.NotNil(t, d)
|
||||
assert.Equal(t, int64(100 * 1024 * 1024), d.size)
|
||||
assert.Equal(t, "/disk.img", d.diskPath)
|
||||
assert.Equal(t, "/mnt", d.mountAt)
|
||||
|
||||
// Ensure by default we get a commander interface returned and that it
|
||||
// returns an *exec.Cmd.
|
||||
o := d.commander(context.TODO(), "foo", "-bar")
|
||||
assert.NotNil(t, o)
|
||||
_, ok := o.(Commander)
|
||||
assert.True(t, ok)
|
||||
_, ok = o.(*exec.Cmd)
|
||||
assert.True(t, ok)
|
||||
})
|
||||
|
||||
t.Run("creates an instance with custom options", func(t *testing.T) {
|
||||
fs := afero.NewMemMapFs()
|
||||
|
||||
cprov := struct {
|
||||
Commander
|
||||
}{}
|
||||
c := func(ctx context.Context, name string, args ...string) Commander {
|
||||
return &cprov
|
||||
}
|
||||
|
||||
d := New(100, "/disk.img", "/mnt", WithFs(fs), WithCommander(c))
|
||||
assert.NotNil(t, d)
|
||||
assert.Same(t, fs, d.fs)
|
||||
assert.Same(t, &cprov, d.commander(context.TODO(), ""))
|
||||
})
|
||||
|
||||
t.Run("panics if either path is empty", func(t *testing.T) {
|
||||
assert.Panics(t, func() {
|
||||
_ = New(100, "", "/bar")
|
||||
})
|
||||
|
||||
assert.Panics(t, func() {
|
||||
_ = New(100, "/foo", "")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestDisk_Exists(t *testing.T) {
|
||||
t.Run("it exists", func(t *testing.T) {
|
||||
d := newMockDisk(nil)
|
||||
f, err := d.fs.Create("/disk.img")
|
||||
require.NoError(t, err)
|
||||
_ = f.Close()
|
||||
|
||||
exists, err := d.Exists()
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exists)
|
||||
})
|
||||
|
||||
t.Run("it does not exist", func(t *testing.T) {
|
||||
d := newMockDisk(nil)
|
||||
exists, err := d.Exists()
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, exists)
|
||||
})
|
||||
|
||||
t.Run("it reports errors", func(t *testing.T) {
|
||||
d := newMockDisk(nil)
|
||||
err := d.fs.Mkdir("/disk.img", 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
exists, err := d.Exists()
|
||||
assert.Error(t, err)
|
||||
assert.False(t, exists)
|
||||
assert.EqualError(t, err, ErrInvalidDiskPathTarget.Error())
|
||||
})
|
||||
}
|
||||
|
||||
func TestDisk_IsMounted(t *testing.T) {
|
||||
t.Run("executes command and finds mounted disk", func(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
var called bool
|
||||
|
||||
pctx := context.TODO()
|
||||
var cmd CommanderProvider = func(ctx context.Context, name string, args ...string) Commander {
|
||||
called = true
|
||||
is.Same(pctx, ctx)
|
||||
is.Equal("grep", name)
|
||||
is.Len(args, 3)
|
||||
is.Equal([]string{"-qs", "/mnt ext4", "/proc/mounts"}, args)
|
||||
|
||||
return &mockCmd{}
|
||||
}
|
||||
|
||||
disk := newMockDisk(cmd)
|
||||
mnt, err := disk.IsMounted(pctx)
|
||||
is.NoError(err)
|
||||
is.True(mnt)
|
||||
is.True(called)
|
||||
})
|
||||
|
||||
t.Run("handles exit code 1 gracefully", func(t *testing.T) {
|
||||
var called bool
|
||||
var cmd CommanderProvider = func(ctx context.Context, name string, args ...string) Commander {
|
||||
called = true
|
||||
return &mockCmd{
|
||||
run: func() error {
|
||||
return &mockedExitCode{code: 1}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
disk := newMockDisk(cmd)
|
||||
mnt, err := disk.IsMounted(context.TODO())
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, mnt)
|
||||
assert.True(t, called)
|
||||
})
|
||||
|
||||
t.Run("handles unexpected errors successfully", func(t *testing.T) {
|
||||
var cmd CommanderProvider = func(ctx context.Context, name string, args ...string) Commander {
|
||||
return &mockCmd{
|
||||
run: func() error {
|
||||
return &mockedExitCode{code: 3}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
disk := newMockDisk(cmd)
|
||||
mnt, err := disk.IsMounted(context.TODO())
|
||||
assert.Error(t, err)
|
||||
assert.False(t, mnt)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDisk_Mount(t *testing.T) {
|
||||
failedCmd := func(ctx context.Context, name string, args ...string) Commander {
|
||||
return &mockCmd{run: func() error {
|
||||
return &mockedExitCode{code: 1}
|
||||
}}
|
||||
}
|
||||
|
||||
t.Run("error is returned if mount point is not a directory", func(t *testing.T) {
|
||||
disk := newMockDisk(failedCmd)
|
||||
_, err := disk.fs.Create("/mnt")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = disk.Mount(context.TODO())
|
||||
assert.Error(t, err)
|
||||
assert.EqualError(t, err, ErrMountPathNotDirectory.Error())
|
||||
})
|
||||
|
||||
t.Run("error is returned if mount point cannot be created", func(t *testing.T) {
|
||||
disk := newMockDisk(failedCmd)
|
||||
disk.fs = afero.NewReadOnlyFs(disk.fs)
|
||||
|
||||
err := disk.Mount(context.TODO())
|
||||
assert.Error(t, err)
|
||||
assert.EqualError(t, err, "vhd: failed to create mount path: operation not permitted")
|
||||
})
|
||||
|
||||
t.Run("error is returned if already mounted", func(t *testing.T) {
|
||||
disk := newMockDisk(nil)
|
||||
err := disk.Mount(context.TODO())
|
||||
assert.Error(t, err)
|
||||
assert.EqualError(t, err, ErrFilesystemMounted.Error())
|
||||
})
|
||||
|
||||
t.Run("error is returned if mount command fails", func(t *testing.T) {
|
||||
var called bool
|
||||
var cmd CommanderProvider = func(ctx context.Context, name string, args ...string) Commander {
|
||||
return &mockCmd{
|
||||
run: func() error {
|
||||
return &mockedExitCode{code: 1}
|
||||
},
|
||||
output: func() ([]byte, error) {
|
||||
called = true
|
||||
|
||||
assert.Equal(t, "mount", name)
|
||||
assert.Equal(t, []string{"-t", "auto", "-o", "loop", "/disk.img", "/mnt"}, args)
|
||||
|
||||
return nil, &exec.ExitError{
|
||||
ProcessState: &os.ProcessState{},
|
||||
Stderr: []byte("foo bar.\n"),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
disk := newMockDisk(cmd)
|
||||
err := disk.Mount(context.TODO())
|
||||
assert.Error(t, err)
|
||||
assert.EqualError(t, err, "vhd: failed to mount disk: foo bar: exit status 0")
|
||||
assert.True(t, called)
|
||||
})
|
||||
|
||||
t.Run("disk can be mounted at existing path", func(t *testing.T) {
|
||||
disk := newMockDisk(failedCmd)
|
||||
require.NoError(t, disk.fs.Mkdir("/mnt", 0600))
|
||||
|
||||
err := disk.Mount(context.TODO())
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("disk can be mounted at non-existing path", func(t *testing.T) {
|
||||
disk := newMockDisk(failedCmd)
|
||||
err := disk.Mount(context.TODO())
|
||||
assert.NoError(t, err)
|
||||
|
||||
st, err := disk.fs.Stat("/mnt")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, st.IsDir())
|
||||
})
|
||||
}
|
||||
|
||||
func TestDisk_Unmount(t *testing.T) {
|
||||
t.Run("can unmount a disk", func(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
pctx := context.TODO()
|
||||
|
||||
var called bool
|
||||
var cmd CommanderProvider = func(ctx context.Context, name string, args ...string) Commander {
|
||||
called = true
|
||||
|
||||
is.Same(pctx, ctx)
|
||||
is.Equal("umount", name)
|
||||
is.Equal([]string{"/mnt"}, args)
|
||||
|
||||
return &mockCmd{}
|
||||
}
|
||||
|
||||
disk := newMockDisk(cmd)
|
||||
err := disk.Unmount(pctx)
|
||||
is.NoError(err)
|
||||
is.True(called)
|
||||
})
|
||||
|
||||
t.Run("handles exit code 32 correctly", func(t *testing.T) {
|
||||
var cmd CommanderProvider = func(ctx context.Context, name string, args ...string) Commander {
|
||||
return &mockCmd{
|
||||
run: func() error {
|
||||
return &mockedExitCode{code: 32}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
disk := newMockDisk(cmd)
|
||||
err := disk.Unmount(context.TODO())
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("non code 32 errors are returned as error", func(t *testing.T) {
|
||||
var cmd CommanderProvider = func(ctx context.Context, name string, args ...string) Commander {
|
||||
return &mockCmd{
|
||||
run: func() error {
|
||||
return &mockedExitCode{code: 1}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
disk := newMockDisk(cmd)
|
||||
err := disk.Unmount(context.TODO())
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("errors without ExitCode function are returned", func(t *testing.T) {
|
||||
var cmd CommanderProvider = func(ctx context.Context, name string, args ...string) Commander {
|
||||
return &mockCmd{
|
||||
run: func() error {
|
||||
return errors.New("foo bar")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
disk := newMockDisk(cmd)
|
||||
err := disk.Unmount(context.TODO())
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDisk_Allocate(t *testing.T) {
|
||||
t.Run("disk is unmounted before allocating space", func(t *testing.T) {
|
||||
var called bool
|
||||
var cmd CommanderProvider = func(ctx context.Context, name string, args ...string) Commander {
|
||||
return &mockCmd{
|
||||
output: func() ([]byte, error) {
|
||||
called = true
|
||||
assert.Equal(t, "fallocate", name)
|
||||
assert.Equal(t, []string{"-l", "102400K", "/disk.img"}, args)
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
disk := newMockDisk(cmd)
|
||||
err := disk.fs.Mkdir("/mnt", 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = disk.Allocate(context.TODO())
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, called)
|
||||
})
|
||||
|
||||
t.Run("disk space is allocated even when not exists", func(t *testing.T) {
|
||||
disk := newMockDisk(nil)
|
||||
err := disk.Allocate(context.TODO())
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("error is returned if command fails", func(t *testing.T) {
|
||||
var cmd CommanderProvider = func(ctx context.Context, name string, args ...string) Commander {
|
||||
return &mockCmd{
|
||||
output: func() ([]byte, error) {
|
||||
return nil, &exec.ExitError{
|
||||
ProcessState: &os.ProcessState{},
|
||||
Stderr: []byte("foo bar.\n"),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
disk := newMockDisk(cmd)
|
||||
_, err := disk.fs.Create("/disk.img")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = disk.Allocate(context.TODO())
|
||||
assert.Error(t, err)
|
||||
assert.EqualError(t, err, "vhd: failed to execute fallocate command: foo bar: exit status 0")
|
||||
})
|
||||
}
|
||||
|
||||
func TestDisk_MakeFilesystem(t *testing.T) {
|
||||
t.Run("filesystem is created if not found in /etc/fstab", func(t *testing.T) {
|
||||
var called bool
|
||||
var cmd CommanderProvider = func(ctx context.Context, name string, args ...string) Commander {
|
||||
return &mockCmd{
|
||||
run: func() error {
|
||||
// Expect the call from IsMounted here and just return what we need
|
||||
// to indicate that nothing is currently mounted.
|
||||
if name == "grep" {
|
||||
return &mockedExitCode{code: 1}
|
||||
}
|
||||
called = true
|
||||
assert.Equal(t, "mkfs", name)
|
||||
assert.Equal(t, []string{"-t", "ext4", "/disk.img"}, args)
|
||||
return nil
|
||||
},
|
||||
output: func() ([]byte, error) {
|
||||
return nil, errors.New("error: can't find in /etc/fstab foo bar testing")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
disk := newMockDisk(cmd)
|
||||
err := disk.MakeFilesystem(context.TODO())
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, called)
|
||||
})
|
||||
|
||||
t.Run("filesystem is created if error is returned from mount command", func(t *testing.T) {
|
||||
var called bool
|
||||
var cmd CommanderProvider = func(ctx context.Context, name string, args ...string) Commander {
|
||||
return &mockCmd{
|
||||
run: func() error {
|
||||
// Expect the call from IsMounted here and just return what we need
|
||||
// to indicate that nothing is currently mounted.
|
||||
if name == "grep" {
|
||||
return &mockedExitCode{code: 1}
|
||||
}
|
||||
called = true
|
||||
assert.Equal(t, "mkfs", name)
|
||||
assert.Equal(t, []string{"-t", "ext4", "/disk.img"}, args)
|
||||
return nil
|
||||
},
|
||||
output: func() ([]byte, error) {
|
||||
if name == "mount" {
|
||||
return nil, &exec.ExitError{
|
||||
Stderr: []byte("foo bar: exit status 32\n"),
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
disk := newMockDisk(cmd)
|
||||
err := disk.MakeFilesystem(context.TODO())
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, called)
|
||||
})
|
||||
|
||||
t.Run("error is returned if currently mounted", func(t *testing.T) {
|
||||
disk := newMockDisk(nil)
|
||||
err := disk.MakeFilesystem(context.TODO())
|
||||
assert.Error(t, err)
|
||||
assert.EqualError(t, err, ErrFilesystemExists.Error())
|
||||
})
|
||||
}
|
|
@ -17,6 +17,7 @@ import (
|
|||
"github.com/cenkalti/backoff/v4"
|
||||
"github.com/goccy/go-json"
|
||||
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/pterodactyl/wings/system"
|
||||
)
|
||||
|
||||
|
@ -59,6 +60,18 @@ func New(base string, opts ...ClientOption) Client {
|
|||
return &c
|
||||
}
|
||||
|
||||
// NewFromConfig returns a new Client using the configuration passed through
|
||||
// by the caller.
|
||||
func NewFromConfig(cfg *config.Configuration, opts ...ClientOption) Client {
|
||||
passOpts := []ClientOption{
|
||||
WithCredentials(cfg.AuthenticationTokenId, cfg.AuthenticationToken),
|
||||
WithHttpClient(&http.Client{
|
||||
Timeout: time.Second * time.Duration(cfg.RemoteQuery.Timeout),
|
||||
}),
|
||||
}
|
||||
return New(cfg.PanelLocation, append(passOpts, opts...)...)
|
||||
}
|
||||
|
||||
// WithCredentials sets the credentials to use when making request to the remote
|
||||
// API endpoint.
|
||||
func WithCredentials(id, token string) ClientOption {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package filesystem
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/pterodactyl/wings/internal/vhd"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
|
@ -35,18 +37,46 @@ func (ult *usageLookupTime) Get() time.Time {
|
|||
return ult.value
|
||||
}
|
||||
|
||||
// Returns the maximum amount of disk space that this Filesystem instance is allowed to use.
|
||||
// MaxDisk returns the maximum amount of disk space that this Filesystem
|
||||
// instance is allowed to use.
|
||||
func (fs *Filesystem) MaxDisk() int64 {
|
||||
return atomic.LoadInt64(&fs.diskLimit)
|
||||
}
|
||||
|
||||
// Sets the disk space limit for this Filesystem instance.
|
||||
func (fs *Filesystem) SetDiskLimit(i int64) {
|
||||
atomic.SwapInt64(&fs.diskLimit, i)
|
||||
// SetDiskLimit sets the disk space limit for this Filesystem instance. This
|
||||
// logic will also handle mounting or unmounting a virtual disk if it is being
|
||||
// used currently.
|
||||
func (fs *Filesystem) SetDiskLimit(ctx context.Context, i int64) error {
|
||||
// Do nothing if this method is called but the limit is not changing.
|
||||
if atomic.LoadInt64(&fs.diskLimit) == i {
|
||||
return nil
|
||||
}
|
||||
if vhd.Enabled() {
|
||||
if i == 0 && fs.IsVirtual() {
|
||||
fs.log().Debug("disk limit changed to 0, destroying virtual disk")
|
||||
// Remove the VHD if it is mounted so that we're just storing files directly on the system
|
||||
// since we cannot have a virtual disk with a space limit enforced like that.
|
||||
if err := fs.vhd.Destroy(ctx); err != nil {
|
||||
return errors.WithStackIf(err)
|
||||
}
|
||||
fs.vhd = nil
|
||||
}
|
||||
// If we're setting a disk size go ahead and mount the VHD if it isn't already mounted,
|
||||
// and then allocate the new space to the disk.
|
||||
if i > 0 {
|
||||
fs.log().Debug("disk limit updated, allocating new space to virtual disk")
|
||||
if err := fs.ConfigureDisk(ctx, i); err != nil {
|
||||
return errors.WithStackIf(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
fs.log().WithField("limit", i).Debug("disk limit updated")
|
||||
atomic.StoreInt64(&fs.diskLimit, i)
|
||||
return nil
|
||||
}
|
||||
|
||||
// The same concept as HasSpaceAvailable however this will return an error if there is
|
||||
// no space, rather than a boolean value.
|
||||
// HasSpaceErr is the same concept as HasSpaceAvailable however this will return
|
||||
// an error if there is no space, rather than a boolean value.
|
||||
func (fs *Filesystem) HasSpaceErr(allowStaleValue bool) error {
|
||||
if !fs.HasSpaceAvailable(allowStaleValue) {
|
||||
return newFilesystemError(ErrCodeDiskSpace, nil)
|
||||
|
@ -54,67 +84,77 @@ func (fs *Filesystem) HasSpaceErr(allowStaleValue bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Determines if the directory a file is trying to be added to has enough space available
|
||||
// for the file to be written to.
|
||||
// HasSpaceAvailable determines if the directory a file is trying to be added to
|
||||
// has enough space available for the file to be written to.
|
||||
//
|
||||
// Because determining the amount of space being used by a server is a taxing operation we
|
||||
// will load it all up into a cache and pull from that as long as the key is not expired.
|
||||
// Because determining the amount of space being used by a server is a taxing
|
||||
// operation we will load it all up into a cache and pull from that as long as
|
||||
// the key is not expired. This operation will potentially block unless
|
||||
// allowStaleValue is set to true. See the documentation on DiskUsage for how
|
||||
// this affects the call.
|
||||
//
|
||||
// This operation will potentially block unless allowStaleValue is set to true. See the
|
||||
// documentation on DiskUsage for how this affects the call.
|
||||
// If the current size of the disk is larger than the maximum allowed size this
|
||||
// function will return false, in all other cases it will return true. We do
|
||||
// not check the existence of a virtual disk at this point since this logic is
|
||||
// used to return friendly error messages to users, and also prevent us wasting
|
||||
// time on more taxing operations when we know the result will end up failing due
|
||||
// to space limits.
|
||||
//
|
||||
// If the servers disk limit is set to 0 it means there is no limit, however the
|
||||
// DiskUsage method is still called to keep the cache warm. This function will
|
||||
// always return true for a server with no limit set.
|
||||
func (fs *Filesystem) HasSpaceAvailable(allowStaleValue bool) bool {
|
||||
size, err := fs.DiskUsage(allowStaleValue)
|
||||
if err != nil {
|
||||
log.WithField("root", fs.root).WithField("error", err).Warn("failed to determine root fs directory size")
|
||||
fs.log().WithField("error", err).Warn("failed to determine root fs directory size")
|
||||
}
|
||||
return fs.MaxDisk() == 0 || size <= fs.MaxDisk()
|
||||
}
|
||||
|
||||
// If space is -1 or 0 just return true, means they're allowed unlimited.
|
||||
//
|
||||
// Technically we could skip disk space calculation because we don't need to check if the
|
||||
// server exceeds its limit but because this method caches the disk usage it would be best
|
||||
// to calculate the disk usage and always return true.
|
||||
if fs.MaxDisk() == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
return size <= fs.MaxDisk()
|
||||
}
|
||||
|
||||
// Returns the cached value for the amount of disk space used by the filesystem. Do not rely on this
|
||||
// function for critical logical checks. It should only be used in areas where the actual disk usage
|
||||
// does not need to be perfect, e.g. API responses for server resource usage.
|
||||
// CachedUsage returns the cached value for the amount of disk space used by the
|
||||
// filesystem. Do not rely on this function for critical logical checks. It
|
||||
// should only be used in areas where the actual disk usage does not need to be
|
||||
// perfect, e.g. API responses for server resource usage.
|
||||
func (fs *Filesystem) CachedUsage() int64 {
|
||||
return atomic.LoadInt64(&fs.diskUsed)
|
||||
}
|
||||
|
||||
// Internal helper function to allow other parts of the codebase to check the total used disk space
|
||||
// as needed without overly taxing the system. This will prioritize the value from the cache to avoid
|
||||
// excessive IO usage. We will only walk the filesystem and determine the size of the directory if there
|
||||
// DiskUsage is an internal helper function to allow other parts of the codebase
|
||||
// to check the total used disk space as needed without overly taxing the system.
|
||||
// This will prioritize the value from the cache to avoid excessive IO usage. We
|
||||
// will only walk the filesystem and determine the size of the directory if there
|
||||
// is no longer a cached value.
|
||||
//
|
||||
// If "allowStaleValue" is set to true, a stale value MAY be returned to the caller if there is an
|
||||
// expired cache value AND there is currently another lookup in progress. If there is no cached value but
|
||||
// no other lookup is in progress, a fresh disk space response will be returned to the caller.
|
||||
// If "allowStaleValue" is set to true, a stale value MAY be returned to the
|
||||
// caller if there is an expired cache value AND there is currently another
|
||||
// lookup in progress. If there is no cached value but no other lookup is in
|
||||
// progress, a fresh disk space response will be returned to the caller.
|
||||
//
|
||||
// This is primarily to avoid a bunch of I/O operations from piling up on the server, especially on servers
|
||||
// with a large amount of files.
|
||||
// This is primarily to avoid a bunch of I/O operations from piling up on the
|
||||
// server, especially on servers with a large amount of files.
|
||||
func (fs *Filesystem) DiskUsage(allowStaleValue bool) (int64, error) {
|
||||
// A disk check interval of 0 means this functionality is completely disabled.
|
||||
if fs.diskCheckInterval == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if !fs.lastLookupTime.Get().After(time.Now().Add(time.Second * fs.diskCheckInterval * -1)) {
|
||||
since := time.Now().Add(time.Second * fs.diskCheckInterval * -1)
|
||||
// If the last lookup time was before our calculated limit we will re-execute this
|
||||
// checking logic. If the lookup time was after the oldest possible timestamp we will
|
||||
// continue returning the cached value.
|
||||
if fs.lastLookupTime.Get().Before(since) {
|
||||
// If we are now allowing a stale response go ahead and perform the lookup and return the fresh
|
||||
// value. This is a blocking operation to the calling process.
|
||||
if !allowStaleValue {
|
||||
return fs.updateCachedDiskUsage()
|
||||
} else if !fs.lookupInProgress.Load() {
|
||||
}
|
||||
|
||||
// Otherwise, if we allow a stale value and there isn't a valid item in the cache and we aren't
|
||||
// currently performing a lookup, just do the disk usage calculation in the background.
|
||||
if !fs.lookupInProgress.Load() {
|
||||
go func(fs *Filesystem) {
|
||||
if _, err := fs.updateCachedDiskUsage(); err != nil {
|
||||
log.WithField("root", fs.root).WithField("error", err).Warn("failed to update fs disk usage from within routine")
|
||||
fs.log().WithField("error", err).Warn("failed to update fs disk usage from within routine")
|
||||
}
|
||||
}(fs)
|
||||
}
|
||||
|
@ -194,11 +234,14 @@ func (fs *Filesystem) DirectorySize(dir string) (int64, error) {
|
|||
return size, errors.WrapIf(err, "server/filesystem: directorysize: failed to walk directory")
|
||||
}
|
||||
|
||||
// Helper function to determine if a server has space available for a file of a given size.
|
||||
// If space is available, no error will be returned, otherwise an ErrNotEnoughSpace error
|
||||
// will be raised.
|
||||
// HasSpaceFor is a function to determine if a server has space available for a
|
||||
// file of a given size. If space is available, no error will be returned,
|
||||
// otherwise an ErrNotEnoughSpace error will be raised. If this filesystem is
|
||||
// configured as a virtual disk this function is a no-op as we will fall through
|
||||
// to the native implementation to throw back an error if there is not disk
|
||||
// space available.
|
||||
func (fs *Filesystem) HasSpaceFor(size int64) error {
|
||||
if fs.MaxDisk() == 0 {
|
||||
if fs.IsVirtual() || fs.MaxDisk() == 0 {
|
||||
return nil
|
||||
}
|
||||
s, err := fs.DiskUsage(true)
|
||||
|
@ -234,3 +277,7 @@ func (fs *Filesystem) addDisk(i int64) int64 {
|
|||
|
||||
return atomic.AddInt64(&fs.diskUsed, i)
|
||||
}
|
||||
|
||||
func (fs *Filesystem) log() *log.Entry {
|
||||
return log.WithField("server", fs.uuid).WithField("root", fs.root)
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
ignore "github.com/sabhiram/go-gitignore"
|
||||
|
||||
"github.com/pterodactyl/wings/config"
|
||||
"github.com/pterodactyl/wings/internal/vhd"
|
||||
"github.com/pterodactyl/wings/system"
|
||||
)
|
||||
|
||||
|
@ -30,19 +31,23 @@ type Filesystem struct {
|
|||
diskUsed int64
|
||||
diskCheckInterval time.Duration
|
||||
denylist *ignore.GitIgnore
|
||||
vhd *vhd.Disk
|
||||
|
||||
// The maximum amount of disk space (in bytes) that this Filesystem instance can use.
|
||||
diskLimit int64
|
||||
|
||||
// The root data directory path for this Filesystem instance.
|
||||
root string
|
||||
uuid string
|
||||
|
||||
isTest bool
|
||||
}
|
||||
|
||||
// New creates a new Filesystem instance for a given server.
|
||||
func New(root string, size int64, denylist []string) *Filesystem {
|
||||
return &Filesystem{
|
||||
func New(uuid string, size int64, denylist []string) *Filesystem {
|
||||
root := filepath.Join(config.Get().System.Data, uuid)
|
||||
fs := Filesystem{
|
||||
uuid: uuid,
|
||||
root: root,
|
||||
diskLimit: size,
|
||||
diskCheckInterval: time.Duration(config.Get().System.DiskCheckInterval),
|
||||
|
@ -50,6 +55,15 @@ func New(root string, size int64, denylist []string) *Filesystem {
|
|||
lookupInProgress: system.NewAtomicBool(false),
|
||||
denylist: ignore.CompileIgnoreLines(denylist...),
|
||||
}
|
||||
|
||||
// If VHD support is enabled but this server is configured with no disk size
|
||||
// limit we cannot actually use a virtual disk. In that case fall back to using
|
||||
// the default driver.
|
||||
if vhd.Enabled() && size > 0 {
|
||||
fs.vhd = vhd.New(size, vhd.DiskPath(uuid), fs.root)
|
||||
}
|
||||
|
||||
return &fs
|
||||
}
|
||||
|
||||
// Path returns the root path for the Filesystem instance.
|
||||
|
@ -77,9 +91,9 @@ func (fs *Filesystem) File(p string) (*os.File, Stat, error) {
|
|||
return f, st, nil
|
||||
}
|
||||
|
||||
// Acts by creating the given file and path on the disk if it is not present already. If
|
||||
// it is present, the file is opened using the defaults which will truncate the contents.
|
||||
// The opened file is then returned to the caller.
|
||||
// Touch acts by creating the given file and path on the disk if it is not present
|
||||
// already. If it is present, the file is opened using the defaults which will
|
||||
// truncate the contents. The opened file is then returned to the caller.
|
||||
func (fs *Filesystem) Touch(p string, flag int) (*os.File, error) {
|
||||
cleaned, err := fs.SafePath(p)
|
||||
if err != nil {
|
||||
|
@ -155,6 +169,12 @@ func (fs *Filesystem) Writefile(p string, r io.Reader) error {
|
|||
|
||||
buf := make([]byte, 1024*4)
|
||||
sz, err := io.CopyBuffer(file, r, buf)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no space left on device") {
|
||||
return newFilesystemError(ErrCodeDiskSpace, err)
|
||||
}
|
||||
return errors.WrapIf(err, "filesystem: failed to copy buffer for file write")
|
||||
}
|
||||
|
||||
// Adjust the disk usage to account for the old size and the new size of the file.
|
||||
fs.addDisk(sz - currentSize)
|
||||
|
@ -312,8 +332,9 @@ func (fs *Filesystem) findCopySuffix(dir string, name string, extension string)
|
|||
return name + suffix + extension, nil
|
||||
}
|
||||
|
||||
// Copies a given file to the same location and appends a suffix to the file to indicate that
|
||||
// it has been copied.
|
||||
// Copy takes a given input file path and creates a copy of the file at the same
|
||||
// location, appending a unique number to the end. For example, a copy of "test.txt"
|
||||
// would create "test 2.txt" as the copy, then "test 3.txt" and so on.
|
||||
func (fs *Filesystem) Copy(p string) error {
|
||||
cleaned, err := fs.SafePath(p)
|
||||
if err != nil {
|
||||
|
|
42
server/filesystem/virtual.go
Normal file
42
server/filesystem/virtual.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
package filesystem
|
||||
|
||||
import (
|
||||
"context"
|
||||
"emperror.dev/errors"
|
||||
"github.com/pterodactyl/wings/internal/vhd"
|
||||
)
|
||||
|
||||
// IsVirtual returns true if the filesystem is currently using a virtual disk.
|
||||
func (fs *Filesystem) IsVirtual() bool {
|
||||
return fs.vhd != nil
|
||||
}
|
||||
|
||||
// ConfigureDisk will attempt to create a new VHD if there is not one already
|
||||
// created for the filesystem. If there is this method will attempt to resize
|
||||
// the underlying data volume. Passing a size of 0 or less will panic.
|
||||
func (fs *Filesystem) ConfigureDisk(ctx context.Context, size int64) error {
|
||||
if size <= 0 {
|
||||
panic("filesystem: attempt to configure disk with empty size")
|
||||
}
|
||||
if fs.vhd == nil {
|
||||
fs.vhd = vhd.New(size, vhd.DiskPath(fs.uuid), fs.root)
|
||||
if err := fs.MountDisk(ctx); err != nil {
|
||||
return errors.WithStackIf(err)
|
||||
}
|
||||
}
|
||||
// Resize the disk now that it is for sure mounted and exists on the system.
|
||||
if err := fs.vhd.Resize(ctx, size); err != nil {
|
||||
return errors.WithStackIf(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MountDisk will attempt to mount the underlying virtual disk for the server.
|
||||
// If the disk is already mounted this is a no-op function.
|
||||
func (fs *Filesystem) MountDisk(ctx context.Context) error {
|
||||
err := fs.vhd.Mount(ctx)
|
||||
if errors.Is(err, vhd.ErrFilesystemMounted) {
|
||||
return nil
|
||||
}
|
||||
return errors.WrapIf(err, "filesystem: failed to mount VHD")
|
||||
}
|
|
@ -5,7 +5,6 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
@ -25,14 +24,16 @@ import (
|
|||
type Manager struct {
|
||||
mu sync.RWMutex
|
||||
client remote.Client
|
||||
skipVhdInitialization bool
|
||||
servers []*Server
|
||||
}
|
||||
|
||||
// NewManager returns a new server manager instance. This will boot up all the
|
||||
// servers that are currently present on the filesystem and set them into the
|
||||
// manager.
|
||||
func NewManager(ctx context.Context, client remote.Client) (*Manager, error) {
|
||||
func NewManager(ctx context.Context, client remote.Client, skipVhdInit bool) (*Manager, error) {
|
||||
m := NewEmptyManager(client)
|
||||
m.skipVhdInitialization = skipVhdInit
|
||||
if err := m.init(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -184,7 +185,7 @@ func (m *Manager) ReadStates() (map[string]string, error) {
|
|||
// InitServer initializes a server using a data byte array. This will be
|
||||
// marshaled into the given struct using a YAML marshaler. This will also
|
||||
// configure the given environment for a server.
|
||||
func (m *Manager) InitServer(data remote.ServerConfigurationResponse) (*Server, error) {
|
||||
func (m *Manager) InitServer(ctx context.Context, data remote.ServerConfigurationResponse) (*Server, error) {
|
||||
s, err := New(m.client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -196,7 +197,15 @@ func (m *Manager) InitServer(data remote.ServerConfigurationResponse) (*Server,
|
|||
return nil, errors.WithStackIf(err)
|
||||
}
|
||||
|
||||
s.fs = filesystem.New(filepath.Join(config.Get().System.Data, s.ID()), s.DiskSpace(), s.Config().Egg.FileDenylist)
|
||||
s.fs = filesystem.New(s.ID(), s.DiskSpace(), s.Config().Egg.FileDenylist)
|
||||
// If this is a virtual filesystem we need to go ahead and mount the disk
|
||||
// so that everything is accessible.
|
||||
if s.fs.IsVirtual() && !m.skipVhdInitialization {
|
||||
log.WithField("server", s.ID()).Info("mounting virtual disk for server")
|
||||
if err := s.fs.MountDisk(ctx); err != nil {
|
||||
return nil, 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
|
||||
|
@ -258,7 +267,7 @@ func (m *Manager) init(ctx context.Context) error {
|
|||
log.WithField("server", data.Uuid).WithField("error", err).Error("failed to parse server configuration from API response, skipping...")
|
||||
return
|
||||
}
|
||||
s, err := m.InitServer(d)
|
||||
s, err := m.InitServer(ctx, d)
|
||||
if err != nil {
|
||||
log.WithField("server", data.Uuid).WithField("error", err).Error("failed to load server, skipping...")
|
||||
return
|
||||
|
|
|
@ -179,6 +179,8 @@ func (s *Server) Log() *log.Entry {
|
|||
//
|
||||
// This also means mass actions can be performed against servers on the Panel
|
||||
// and they will automatically sync with Wings when the server is started.
|
||||
//
|
||||
// TODO: accept a context value rather than using the server's context.
|
||||
func (s *Server) Sync() error {
|
||||
cfg, err := s.client.GetServerConfiguration(s.Context(), s.ID())
|
||||
if err != nil {
|
||||
|
@ -194,7 +196,9 @@ 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 err := s.fs.SetDiskLimit(s.Context(), s.DiskSpace()); err != nil {
|
||||
return errors.WrapIf(err, "server: failed to sync server configuration from API")
|
||||
}
|
||||
|
||||
s.SyncWithEnvironment()
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user