Compare commits
1 Commits
v1.0.0-rc.
...
v1.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7746a8359 |
2
.github/workflows/build-test.yml
vendored
2
.github/workflows/build-test.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
go-version: '^1.15'
|
go-version: '^1.14.2'
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -ldflags "-X github.com/pterodactyl/wings/system.Version=dev-${GIT_COMMIT:0:7}" -o build/wings_linux_amd64 -v wings.go
|
run: GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -ldflags "-X github.com/pterodactyl/wings/system.Version=dev-${GIT_COMMIT:0:7}" -o build/wings_linux_amd64 -v wings.go
|
||||||
|
|||||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -12,18 +12,18 @@ jobs:
|
|||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
go-version: '^1.15'
|
go-version: '^1.14.2'
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
REF: ${{ github.ref }}
|
REF: ${{ github.ref }}
|
||||||
run: GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -ldflags "-X github.com/pterodactyl/wings/system.Version=${REF:11}" -o build/wings_linux_amd64 -v wings.go
|
run: GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -ldflags "-X github.com/pterodactyl/wings/system.Version=dev-${GIT_COMMIT:0:7}" -o build/wings_linux_amd64 -v wings.go
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: go test ./...
|
run: go test ./...
|
||||||
|
|
||||||
- name: Compress binary and make it executable
|
- name: Compress binary and make it executable
|
||||||
run: upx --brute build/wings_linux_amd64 && chmod +x build/wings_linux_amd64
|
run: upx build/wings_linux_amd64 && chmod +x build/wings_linux_amd64
|
||||||
|
|
||||||
- name: Extract changelog
|
- name: Extract changelog
|
||||||
env:
|
env:
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -1,5 +1,5 @@
|
|||||||
build:
|
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=amd64 go build -ldflags="-s -w" -gcflags "all=-trimpath=/Users/dane/Sites/development/code" -o build/wings_linux_amd64 -v wings.go
|
||||||
|
|
||||||
compress:
|
compress:
|
||||||
upx --brute build/wings_*
|
upx --brute build/wings_*
|
||||||
|
|||||||
43
README.md
43
README.md
@@ -1,35 +1,16 @@
|
|||||||
[](https://pterodactyl.io)
|
# Alpha Project
|
||||||
|
Please refrain from opening PRs or Issues at this time. This project is still under heavy development, and until we have a solid foundation and plan for how everything will connect, we will not be accepting PRs or feature suggestions.
|
||||||
|
|
||||||
[](https://pterodactyl.io/discord)
|
# Pterodactyl wings [](https://travis-ci.org/pterodactyl/wings) [](https://www.codacy.com/app/schrej/wings/dashboard) [](https://www.codacy.com/app/schrej/wings/files)
|
||||||
|
|
||||||
# Pterodactyl Wings
|
```
|
||||||
Wings is Pterodactyl's server control plane, built for the rapidly changing gaming industry and designed to be
|
____
|
||||||
highly performant and secure. Wings provides an HTTP API allowing you to interface directly with running server
|
__ Pterodactyl _____/___/_______ _______ ______
|
||||||
instances, fetch server logs, generate backups, and control all aspects of the server lifecycle.
|
\_____\ \/\/ / / / __ / ___/
|
||||||
|
\___\ / / / / /_/ /___ /
|
||||||
|
\___/\___/___/___/___/___ /______/
|
||||||
|
/_______/ alpha
|
||||||
|
```
|
||||||
|
|
||||||
In addition, Wings ships with a built-in SFTP server allowing your system to remain free of Pterodactyl specific
|
A new generation of the Pterodactyl daemon, written in go.
|
||||||
dependencies, and allowing users to authenticate with the same credentials they would normally use to access the Panel.
|
|
||||||
|
|
||||||
## Sponsors
|
|
||||||
I would like to extend my sincere thanks to the following sponsors for helping find Pterodactyl's developement.
|
|
||||||
[Interested in becoming a sponsor?](https://github.com/sponsors/DaneEveritt)
|
|
||||||
|
|
||||||
| Company | About |
|
|
||||||
| ------- | ----- |
|
|
||||||
| [**BloomVPS**](https://bloomvps.com) | BloomVPS offers dedicated core VPS and Minecraft hosting with Ryzen 9 processors. With owned-hardware, we offer truly unbeatable prices on high-performance hosting. |
|
|
||||||
| [**VersatileNode**](https://versatilenode.com/) | Looking to host a minecraft server, vps, or a website? VersatileNode is one of the most affordable hosting providers to provide quality yet cheap services with incredible support. |
|
|
||||||
| [**MineStrator**](https://minestrator.com/) | Looking for a French highend hosting company for you minecraft server? More than 14,000 members on our discord, trust us. |
|
|
||||||
| [**DedicatedMC**](https://dedicatedmc.io/) | DedicatedMC provides Raw Power hosting at affordable pricing, making sure to never compromise on your performance and giving you the best performance money can buy. |
|
|
||||||
| [**Skynode**](https://www.skynode.pro/) | Skynode provides blazing fast game servers along with a top-notch user experience. Whatever our clients are looking for, we're able to provide it! |
|
|
||||||
| [**XCORE-SERVER.de**](https://xcore-server.de/) | XCORE-SERVER.de offers High-End Servers for hosting and gaming since 2012. Fast, excellent and well-known for eSports Gaming. |
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
* [Panel Documentation](https://pterodactyl.io/panel/1.0/getting_started.html)
|
|
||||||
* [Wings Documentation](https://pterodactyl.io/wings/1.0/installing.html)
|
|
||||||
* [Community Guides](https://pterodactyl.io/community/about.html)
|
|
||||||
* Or, get additional help [via Discord](https://discord.gg/pterodactyl)
|
|
||||||
|
|
||||||
## Reporting Issues
|
|
||||||
Please use the [pterodactyl/panel](https://github.com/pterodactyl/panel) repository to report any issues or make
|
|
||||||
feature requests for Wings. In addition, the [security policy](https://github.com/pterodactyl/panel/security/policy) listed
|
|
||||||
within that repository also applies to Wings.
|
|
||||||
@@ -144,7 +144,7 @@ type RequestError struct {
|
|||||||
|
|
||||||
// Returns the error response in a string form that can be more easily consumed.
|
// Returns the error response in a string form that can be more easily consumed.
|
||||||
func (re *RequestError) Error() string {
|
func (re *RequestError) Error() string {
|
||||||
return fmt.Sprintf("Error response from Panel: %s: %s (HTTP/%s)", re.Code, re.Detail, re.Status)
|
return fmt.Sprintf("%s: %s (HTTP/%s)", re.Code, re.Detail, re.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (re *RequestError) String() string {
|
func (re *RequestError) String() string {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
|
|
||||||
type BackupRequest struct {
|
type BackupRequest struct {
|
||||||
Checksum string `json:"checksum"`
|
Checksum string `json:"checksum"`
|
||||||
ChecksumType string `json:"checksum_type"`
|
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
Successful bool `json:"successful"`
|
Successful bool `json:"successful"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/apex/log"
|
|
||||||
"github.com/pterodactyl/wings/parser"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type OutputLineMatcher struct {
|
|
||||||
// The raw string to match against. This may or may not be prefixed with
|
|
||||||
// regex: which indicates we want to match against the regex expression.
|
|
||||||
raw string
|
|
||||||
reg *regexp.Regexp
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine if a given string "s" matches the given line.
|
|
||||||
func (olm *OutputLineMatcher) Matches(s string) bool {
|
|
||||||
if olm.reg == nil {
|
|
||||||
return strings.Contains(s, olm.raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
return olm.reg.MatchString(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the matcher's raw comparison string.
|
|
||||||
func (olm *OutputLineMatcher) String() string {
|
|
||||||
return olm.raw
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unmarshal the startup lines into individual structs for easier matching abilities.
|
|
||||||
func (olm *OutputLineMatcher) UnmarshalJSON(data []byte) error {
|
|
||||||
if err := json.Unmarshal(data, &olm.raw); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(olm.raw, "regex:") && len(olm.raw) > 6 {
|
|
||||||
r, err := regexp.Compile(strings.TrimPrefix(olm.raw, "regex:"))
|
|
||||||
if err != nil {
|
|
||||||
log.WithField("error", err).WithField("raw", olm.raw).Warn("failed to compile output line marked as being regex")
|
|
||||||
}
|
|
||||||
|
|
||||||
olm.reg = r
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProcessStopConfiguration struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defines the process configuration for a given server instance. This sets what the
|
|
||||||
// daemon is looking for to mark a server as done starting, what to do when stopping,
|
|
||||||
// and what changes to make to the configuration file for a server.
|
|
||||||
type ProcessConfiguration struct {
|
|
||||||
Startup struct {
|
|
||||||
Done []*OutputLineMatcher `json:"done"`
|
|
||||||
UserInteraction []string `json:"user_interaction"`
|
|
||||||
StripAnsi bool `json:"strip_ansi"`
|
|
||||||
} `json:"startup"`
|
|
||||||
|
|
||||||
Stop ProcessStopConfiguration `json:"stop"`
|
|
||||||
|
|
||||||
ConfigurationFiles []parser.ConfigurationFile `json:"configs"`
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/pterodactyl/wings/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -25,6 +26,21 @@ type ServerConfigurationResponse struct {
|
|||||||
ProcessConfiguration *ProcessConfiguration `json:"process_configuration"`
|
ProcessConfiguration *ProcessConfiguration `json:"process_configuration"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Defines the process configuration for a given server instance. This sets what the
|
||||||
|
// daemon is looking for to mark a server as done starting, what to do when stopping,
|
||||||
|
// and what changes to make to the configuration file for a server.
|
||||||
|
type ProcessConfiguration struct {
|
||||||
|
Startup struct {
|
||||||
|
Done string `json:"done"`
|
||||||
|
UserInteraction []string `json:"userInteraction"`
|
||||||
|
} `json:"startup"`
|
||||||
|
Stop struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
} `json:"stop"`
|
||||||
|
ConfigurationFiles []parser.ConfigurationFile `json:"configs"`
|
||||||
|
}
|
||||||
|
|
||||||
// Defines installation script information for a server process. This is used when
|
// Defines installation script information for a server process. This is used when
|
||||||
// a server is installed for the first time, and when a server is marked for re-installation.
|
// a server is installed for the first time, and when a server is marked for re-installation.
|
||||||
type InstallationScript struct {
|
type InstallationScript struct {
|
||||||
|
|||||||
@@ -2,57 +2,11 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/apex/log"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"regexp"
|
"github.com/pterodactyl/sftp-server"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SftpAuthRequest struct {
|
func (r *PanelRequest) ValidateSftpCredentials(request sftp_server.AuthenticationRequest) (*sftp_server.AuthenticationResponse, error) {
|
||||||
User string `json:"username"`
|
|
||||||
Pass string `json:"password"`
|
|
||||||
IP string `json:"ip"`
|
|
||||||
SessionID []byte `json:"session_id"`
|
|
||||||
ClientVersion []byte `json:"client_version"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SftpAuthResponse struct {
|
|
||||||
Server string `json:"server"`
|
|
||||||
Token string `json:"token"`
|
|
||||||
Permissions []string `json:"permissions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type sftpInvalidCredentialsError struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ice sftpInvalidCredentialsError) Error() string {
|
|
||||||
return "the credentials provided were invalid"
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsInvalidCredentialsError(err error) bool {
|
|
||||||
_, ok := err.(*sftpInvalidCredentialsError)
|
|
||||||
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// Usernames all follow the same format, so don't even bother hitting the API if the username is not
|
|
||||||
// at least in the expected format. This is very basic protection against random bots finding the SFTP
|
|
||||||
// server and sending a flood of usernames.
|
|
||||||
var validUsernameRegexp = regexp.MustCompile(`^(?i)(.+)\.([a-z0-9]{8})$`)
|
|
||||||
|
|
||||||
func (r *PanelRequest) ValidateSftpCredentials(request SftpAuthRequest) (*SftpAuthResponse, error) {
|
|
||||||
// If the username doesn't meet the expected format that the Panel would even recognize just go ahead
|
|
||||||
// and bail out of the process here to avoid accidentally brute forcing the panel if a bot decides
|
|
||||||
// to connect to spam username attempts.
|
|
||||||
if !validUsernameRegexp.MatchString(request.User) {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"subsystem": "sftp",
|
|
||||||
"username": request.User,
|
|
||||||
"ip": request.IP,
|
|
||||||
}).Warn("failed to validate user credentials (invalid format)")
|
|
||||||
|
|
||||||
return nil, new(sftpInvalidCredentialsError)
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := json.Marshal(request)
|
b, err := json.Marshal(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -68,7 +22,7 @@ func (r *PanelRequest) ValidateSftpCredentials(request SftpAuthRequest) (*SftpAu
|
|||||||
|
|
||||||
if r.HasError() {
|
if r.HasError() {
|
||||||
if r.HttpResponseCode() >= 400 && r.HttpResponseCode() < 500 {
|
if r.HttpResponseCode() >= 400 && r.HttpResponseCode() < 500 {
|
||||||
return nil, new(sftpInvalidCredentialsError)
|
return nil, new(sftp_server.InvalidCredentialsError)
|
||||||
}
|
}
|
||||||
|
|
||||||
rerr := errors.New(r.Error().String())
|
rerr := errors.New(r.Error().String())
|
||||||
@@ -76,7 +30,7 @@ func (r *PanelRequest) ValidateSftpCredentials(request SftpAuthRequest) (*SftpAu
|
|||||||
return nil, rerr
|
return nil, rerr
|
||||||
}
|
}
|
||||||
|
|
||||||
response := new(SftpAuthResponse)
|
response := new(sftp_server.AuthenticationResponse)
|
||||||
body, _ := r.ReadBody()
|
body, _ := r.ReadBody()
|
||||||
|
|
||||||
if err := json.Unmarshal(body, response); err != nil {
|
if err := json.Unmarshal(body, response); err != nil {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ func diagnosticsCmdRun(cmd *cobra.Command, args []string) {
|
|||||||
Name: "ReviewBeforeUpload",
|
Name: "ReviewBeforeUpload",
|
||||||
Prompt: &survey.Confirm{
|
Prompt: &survey.Confirm{
|
||||||
Message: "Do you want to review the collected data before uploading to hastebin.com?",
|
Message: "Do you want to review the collected data before uploading to hastebin.com?",
|
||||||
Help: "The data, especially the logs, might contain sensitive information, so you should review it. You will be asked again if you want to upload.",
|
Help: "The data, especially the logs, might contain sensitive information, so you should review it. You will be asked again if you want to uplaod.",
|
||||||
Default: true,
|
Default: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -82,7 +82,7 @@ func diagnosticsCmdRun(cmd *cobra.Command, args []string) {
|
|||||||
_ = dockerInfo
|
_ = dockerInfo
|
||||||
|
|
||||||
output := &strings.Builder{}
|
output := &strings.Builder{}
|
||||||
fmt.Fprintln(output, "Pterodactyl Wings - Diagnostics Report")
|
fmt.Fprintln(output, "Pterodactly Wings - Diagnostics Report")
|
||||||
printHeader(output, "Versions")
|
printHeader(output, "Versions")
|
||||||
fmt.Fprintln(output, "wings:", system.Version)
|
fmt.Fprintln(output, "wings:", system.Version)
|
||||||
if dockerErr == nil {
|
if dockerErr == nil {
|
||||||
@@ -210,7 +210,7 @@ func uploadToHastebin(hbUrl, content string) (string, error) {
|
|||||||
u.Path = path.Join(u.Path, key)
|
u.Path = path.Join(u.Path, key)
|
||||||
return u.String(), nil
|
return u.String(), nil
|
||||||
}
|
}
|
||||||
return "", errors.New("failed to find key in response")
|
return "", errors.New("Couldn't find key in response")
|
||||||
}
|
}
|
||||||
|
|
||||||
func redact(s string) string {
|
func redact(s string) string {
|
||||||
|
|||||||
203
cmd/root.go
203
cmd/root.go
@@ -3,14 +3,9 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/NYTimes/logrotate"
|
|
||||||
"github.com/apex/log/handlers/multi"
|
|
||||||
"github.com/gammazero/workerpool"
|
|
||||||
"golang.org/x/crypto/acme"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
@@ -26,7 +21,9 @@ import (
|
|||||||
"github.com/pterodactyl/wings/server"
|
"github.com/pterodactyl/wings/server"
|
||||||
"github.com/pterodactyl/wings/sftp"
|
"github.com/pterodactyl/wings/sftp"
|
||||||
"github.com/pterodactyl/wings/system"
|
"github.com/pterodactyl/wings/system"
|
||||||
|
"github.com/remeh/sizedwaitgroup"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
var configPath = config.DefaultLocation
|
var configPath = config.DefaultLocation
|
||||||
@@ -114,14 +111,14 @@ func rootCmdRun(*cobra.Command, []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
printLogo()
|
printLogo()
|
||||||
if err := configureLogging(c.System.LogDirectory, c.Debug); err != nil {
|
if err := configureLogging(c.Debug); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.WithField("path", c.GetPath()).Info("loading configuration from path")
|
log.WithField("path", c.GetPath()).Info("loading configuration from path")
|
||||||
if c.Debug {
|
if c.Debug {
|
||||||
log.Debug("running in debug mode")
|
log.Debug("running in debug mode")
|
||||||
log.Warn("certificate checking is disabled")
|
log.Info("certificate checking is disabled")
|
||||||
|
|
||||||
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
|
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
|
||||||
InsecureSkipVerify: true,
|
InsecureSkipVerify: true,
|
||||||
@@ -132,18 +129,14 @@ func rootCmdRun(*cobra.Command, []string) {
|
|||||||
config.SetDebugViaFlag(debug)
|
config.SetDebugViaFlag(debug)
|
||||||
|
|
||||||
if err := c.System.ConfigureDirectories(); err != nil {
|
if err := c.System.ConfigureDirectories(); err != nil {
|
||||||
log.WithField("error", err).Fatal("failed to configure system directories for pterodactyl")
|
log.Fatal("failed to configure system directories for pterodactyl")
|
||||||
return
|
panic(err)
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.System.EnableLogRotation(); err != nil {
|
|
||||||
log.WithField("error", err).Fatal("failed to configure log rotation on the system")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.WithField("username", c.System.Username).Info("checking for pterodactyl system user")
|
log.WithField("username", c.System.Username).Info("checking for pterodactyl system user")
|
||||||
if su, err := c.EnsurePterodactylUser(); err != nil {
|
if su, err := c.EnsurePterodactylUser(); err != nil {
|
||||||
log.WithField("error", err).Fatal("failed to create pterodactyl system user")
|
log.Error("failed to create pterodactyl system user")
|
||||||
|
panic(err)
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
@@ -153,6 +146,13 @@ func rootCmdRun(*cobra.Command, []string) {
|
|||||||
}).Info("configured system user successfully")
|
}).Info("configured system user successfully")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Info("beginning file permission setting on server data directories")
|
||||||
|
if err := c.EnsureFilePermissions(); err != nil {
|
||||||
|
log.WithField("error", err).Error("failed to properly chown data directories")
|
||||||
|
} else {
|
||||||
|
log.Info("finished ensuring file permissions")
|
||||||
|
}
|
||||||
|
|
||||||
if err := server.LoadDirectory(); err != nil {
|
if err := server.LoadDirectory(); err != nil {
|
||||||
log.WithField("error", err).Fatal("failed to load server configurations")
|
log.WithField("error", err).Fatal("failed to load server configurations")
|
||||||
return
|
return
|
||||||
@@ -160,7 +160,7 @@ func rootCmdRun(*cobra.Command, []string) {
|
|||||||
|
|
||||||
if err := environment.ConfigureDocker(&c.Docker); err != nil {
|
if err := environment.ConfigureDocker(&c.Docker); err != nil {
|
||||||
log.WithField("error", err).Fatal("failed to configure docker environment")
|
log.WithField("error", err).Fatal("failed to configure docker environment")
|
||||||
return
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.WriteToDisk(); err != nil {
|
if err := c.WriteToDisk(); err != nil {
|
||||||
@@ -172,16 +172,19 @@ func rootCmdRun(*cobra.Command, []string) {
|
|||||||
log.WithField("server", s.Id()).Info("loaded configuration for server")
|
log.WithField("server", s.Id()).Info("loaded configuration for server")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new workerpool that limits us to 4 servers being bootstrapped at a time
|
// Create a new WaitGroup that limits us to 4 servers being bootstrapped at a time
|
||||||
// on Wings. This allows us to ensure the environment exists, write configurations,
|
// on Wings. This allows us to ensure the environment exists, write configurations,
|
||||||
// and reboot processes without causing a slow-down due to sequential booting.
|
// and reboot processes without causing a slow-down due to sequential booting.
|
||||||
pool := workerpool.New(4)
|
wg := sizedwaitgroup.New(4)
|
||||||
|
|
||||||
for _, serv := range server.GetServers().All() {
|
for _, serv := range server.GetServers().All() {
|
||||||
s := serv
|
wg.Add()
|
||||||
|
|
||||||
|
go func(s *server.Server) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
pool.Submit(func() {
|
|
||||||
s.Log().Info("ensuring server environment exists")
|
s.Log().Info("ensuring server environment exists")
|
||||||
|
|
||||||
// Create a server environment if none exists currently. This allows us to recover from Docker
|
// Create a server environment if none exists currently. This allows us to recover from Docker
|
||||||
// being reinstalled on the host system for example.
|
// being reinstalled on the host system for example.
|
||||||
if err := s.Environment.Create(); err != nil {
|
if err := s.Environment.Create(); err != nil {
|
||||||
@@ -201,10 +204,8 @@ func rootCmdRun(*cobra.Command, []string) {
|
|||||||
// is that it was running, but we see that the container process is not currently running.
|
// is that it was running, but we see that the container process is not currently running.
|
||||||
if r || (!r && s.IsRunning()) {
|
if r || (!r && s.IsRunning()) {
|
||||||
s.Log().Info("detected server is running, re-attaching to process...")
|
s.Log().Info("detected server is running, re-attaching to process...")
|
||||||
|
if err := s.Environment.Start(); err != nil {
|
||||||
s.SetState(environment.ProcessRunningState)
|
s.Log().WithField("error", errors.WithStack(err)).Warn("failed to properly start server detected as already running")
|
||||||
if err := s.Environment.Attach(); err != nil {
|
|
||||||
s.Log().WithField("error", errors.WithStack(err)).Warn("failed to attach to running server environment")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -212,18 +213,15 @@ func rootCmdRun(*cobra.Command, []string) {
|
|||||||
|
|
||||||
// Addresses potentially invalid data in the stored file that can cause Wings to lose
|
// Addresses potentially invalid data in the stored file that can cause Wings to lose
|
||||||
// track of what the actual server state is.
|
// track of what the actual server state is.
|
||||||
_ = s.SetState(environment.ProcessOfflineState)
|
s.SetState(server.ProcessOfflineState)
|
||||||
})
|
}(serv)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait until all of the servers are ready to go before we fire up the SFTP and HTTP servers.
|
// Wait until all of the servers are ready to go before we fire up the HTTP server.
|
||||||
pool.StopWait()
|
wg.Wait()
|
||||||
|
|
||||||
// Initialize the SFTP server.
|
// Initalize SFTP.
|
||||||
if err := sftp.Initialize(c.System); err != nil {
|
sftp.Initialize(c)
|
||||||
log.WithError(err).Fatal("failed to initialize the sftp server")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the archive directory exists.
|
// Ensure the archive directory exists.
|
||||||
if err := os.MkdirAll(c.System.ArchiveDirectory, 0755); err != nil {
|
if err := os.MkdirAll(c.System.ArchiveDirectory, 0755); err != nil {
|
||||||
@@ -242,46 +240,9 @@ func rootCmdRun(*cobra.Command, []string) {
|
|||||||
"host_port": c.Api.Port,
|
"host_port": c.Api.Port,
|
||||||
}).Info("configuring internal webserver")
|
}).Info("configuring internal webserver")
|
||||||
|
|
||||||
// Configure the router.
|
|
||||||
r := router.Configure()
|
r := router.Configure()
|
||||||
|
addr := fmt.Sprintf("%s:%d", c.Api.Host, c.Api.Port)
|
||||||
|
|
||||||
s := &http.Server{
|
|
||||||
Addr: fmt.Sprintf("%s:%d", c.Api.Host, c.Api.Port),
|
|
||||||
Handler: r,
|
|
||||||
|
|
||||||
TLSConfig: &tls.Config{
|
|
||||||
NextProtos: []string{
|
|
||||||
"h2", // enable HTTP/2
|
|
||||||
"http/1.1",
|
|
||||||
},
|
|
||||||
|
|
||||||
// https://blog.cloudflare.com/exposing-go-on-the-internet
|
|
||||||
CipherSuites: []uint16{
|
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
|
||||||
|
|
||||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
|
||||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
|
||||||
|
|
||||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
|
|
||||||
|
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
|
|
||||||
},
|
|
||||||
|
|
||||||
PreferServerCipherSuites: true,
|
|
||||||
|
|
||||||
MinVersion: tls.VersionTLS12,
|
|
||||||
MaxVersion: tls.VersionTLS13,
|
|
||||||
|
|
||||||
CurvePreferences: []tls.CurveID{
|
|
||||||
tls.X25519,
|
|
||||||
tls.CurveP256,
|
|
||||||
},
|
|
||||||
// END https://blog.cloudflare.com/exposing-go-on-the-internet
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the server should run with TLS but using autocert.
|
|
||||||
if useAutomaticTls && len(tlsHostname) > 0 {
|
if useAutomaticTls && len(tlsHostname) > 0 {
|
||||||
m := autocert.Manager{
|
m := autocert.Manager{
|
||||||
Prompt: autocert.AcceptTOS,
|
Prompt: autocert.AcceptTOS,
|
||||||
@@ -290,44 +251,29 @@ func rootCmdRun(*cobra.Command, []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.WithField("hostname", tlsHostname).
|
log.WithField("hostname", tlsHostname).
|
||||||
Info("webserver is now listening with auto-TLS enabled; certificates will be automatically generated by Let's Encrypt")
|
Info("webserver is now listening with auto-TLS enabled; certifcates will be automatically generated by Let's Encrypt")
|
||||||
|
|
||||||
// Hook autocert into the main http server.
|
// We don't use the autotls runner here since we need to specify a port other than 443
|
||||||
s.TLSConfig.GetCertificate = m.GetCertificate
|
// to be using for SSL connections for Wings.
|
||||||
s.TLSConfig.NextProtos = append(s.TLSConfig.NextProtos, acme.ALPNProto) // enable tls-alpn ACME challenges
|
s := &http.Server{Addr: addr, TLSConfig: m.TLSConfig(), Handler: r}
|
||||||
|
|
||||||
// Start the autocert server.
|
go http.ListenAndServe(":http", m.HTTPHandler(nil))
|
||||||
go func() {
|
|
||||||
if err := http.ListenAndServe(":http", m.HTTPHandler(nil)); err != nil {
|
|
||||||
log.WithError(err).Error("failed to serve autocert http server")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Start the main http server with TLS using autocert.
|
|
||||||
if err := s.ListenAndServeTLS("", ""); err != nil {
|
if err := s.ListenAndServeTLS("", ""); err != nil {
|
||||||
log.WithFields(log.Fields{"auto_tls": true, "tls_hostname": tlsHostname, "error": err}).
|
log.WithFields(log.Fields{"auto_tls": true, "tls_hostname": tlsHostname, "error": err}).
|
||||||
Fatal("failed to configure HTTP server using auto-tls")
|
Fatal("failed to configure HTTP server using auto-tls")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
} else if c.Api.Ssl.Enabled {
|
||||||
return
|
if err := r.RunTLS(addr, c.Api.Ssl.CertificateFile, c.Api.Ssl.KeyFile); err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
// Check if main http server should run with TLS.
|
|
||||||
if c.Api.Ssl.Enabled {
|
|
||||||
if err := s.ListenAndServeTLS(c.Api.Ssl.CertificateFile, c.Api.Ssl.KeyFile); err != nil {
|
|
||||||
log.WithFields(log.Fields{"auto_tls": false, "error": err}).Fatal("failed to configure HTTPS server")
|
log.WithFields(log.Fields{"auto_tls": false, "error": err}).Fatal("failed to configure HTTPS server")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
return
|
} else {
|
||||||
}
|
if err := r.Run(addr); err != nil {
|
||||||
|
|
||||||
// Run the main http server without TLS.
|
|
||||||
s.TLSConfig = nil
|
|
||||||
if err := s.ListenAndServe(); err != nil {
|
|
||||||
log.WithField("error", err).Fatal("failed to configure HTTP server")
|
log.WithField("error", err).Fatal("failed to configure HTTP server")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute calls cobra to handle cli commands
|
// Execute calls cobra to handle cli commands
|
||||||
@@ -337,52 +283,45 @@ func Execute() error {
|
|||||||
|
|
||||||
// Configures the global logger for Zap so that we can call it from any location
|
// Configures the global logger for Zap so that we can call it from any location
|
||||||
// in the code without having to pass around a logger instance.
|
// in the code without having to pass around a logger instance.
|
||||||
func configureLogging(logDir string, debug bool) error {
|
func configureLogging(debug bool) error {
|
||||||
if err := os.MkdirAll(path.Join(logDir, "/install"), 0700); err != nil {
|
cfg := zap.NewProductionConfig()
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
p := filepath.Join(logDir, "/wings.log")
|
|
||||||
w, err := logrotate.NewFile(p)
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.Wrap(err, "failed to open process log file"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if debug {
|
if debug {
|
||||||
log.SetLevel(log.DebugLevel)
|
cfg = zap.NewDevelopmentConfig()
|
||||||
} else {
|
|
||||||
log.SetLevel(log.InfoLevel)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.SetHandler(multi.New(
|
cfg.Encoding = "console"
|
||||||
cli.Default,
|
cfg.OutputPaths = []string{
|
||||||
cli.New(w.File, false),
|
"stdout",
|
||||||
))
|
}
|
||||||
|
|
||||||
log.WithField("path", p).Info("writing log files to disk")
|
logger, err := cfg.Build()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.ReplaceGlobals(logger)
|
||||||
|
|
||||||
|
log.SetHandler(cli.Default)
|
||||||
|
log.SetLevel(log.DebugLevel)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prints the wings logo, nothing special here!
|
// Prints the wings logo, nothing special here!
|
||||||
func printLogo() {
|
func printLogo() {
|
||||||
fmt.Printf(colorstring.Color(`
|
fmt.Println()
|
||||||
____
|
fmt.Println(` ____`)
|
||||||
__ [blue][bold]Pterodactyl[reset] _____/___/_______ _______ ______
|
fmt.Println(`__ Pterodactyl _____/___/_______ _______ ______`)
|
||||||
\_____\ \/\/ / / / __ / ___/
|
fmt.Println(`\_____\ \/\/ / / / __ / ___/`)
|
||||||
\___\ / / / / /_/ /___ /
|
fmt.Println(` \___\ / / / / /_/ /___ /`)
|
||||||
\___/\___/___/___/___/___ /______/
|
fmt.Println(` \___/\___/___/___/___/___ /______/`)
|
||||||
/_______/ [bold]v%s[reset]
|
fmt.Println(` /_______/ v` + system.Version)
|
||||||
|
fmt.Println()
|
||||||
Copyright © 2018 - 2020 Dane Everitt & Contributors
|
fmt.Println(`Website: https://pterodactyl.io`)
|
||||||
|
fmt.Println(`Source: https://github.com/pterodactyl/wings`)
|
||||||
Website: https://pterodactyl.io
|
fmt.Println()
|
||||||
Source: https://github.com/pterodactyl/wings
|
fmt.Println(`Copyright © 2018 - 2020 Dane Everitt & Contributors`)
|
||||||
License: https://github.com/pterodactyl/wings/blob/develop/LICENSE
|
fmt.Println()
|
||||||
|
|
||||||
This software is made available under the terms of the MIT license.
|
|
||||||
The above copyright notice and this permission notice shall be included
|
|
||||||
in all copies or substantial portions of the Software.%s`), system.Version, "\n\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func exitWithConfigurationNotice() {
|
func exitWithConfigurationNotice() {
|
||||||
|
|||||||
118
config/config.go
118
config/config.go
@@ -1,16 +1,19 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/apex/log"
|
||||||
"github.com/cobaugh/osrelease"
|
"github.com/cobaugh/osrelease"
|
||||||
"github.com/creasty/defaults"
|
"github.com/creasty/defaults"
|
||||||
"github.com/gbrlsnchs/jwt/v3"
|
"github.com/gbrlsnchs/jwt/v3"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/user"
|
"os/user"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -55,20 +58,31 @@ type Configuration struct {
|
|||||||
|
|
||||||
// Defines internal throttling configurations for server processes to prevent
|
// Defines internal throttling configurations for server processes to prevent
|
||||||
// someone from running an endless loop that spams data to logs.
|
// someone from running an endless loop that spams data to logs.
|
||||||
Throttles ConsoleThrottles
|
Throttles struct {
|
||||||
|
// The number of data overage warnings (inclusive) that can accumulate
|
||||||
|
// before a process is terminated.
|
||||||
|
KillAtCount int `default:"5" yaml:"kill_at_count"`
|
||||||
|
|
||||||
|
// The number of seconds that must elapse before the internal counter
|
||||||
|
// begins decrementing warnings assigned to a process that is outputting
|
||||||
|
// too much data.
|
||||||
|
DecaySeconds int `default:"10" json:"decay" yaml:"decay"`
|
||||||
|
|
||||||
|
// The total number of bytes allowed to be output by a server process
|
||||||
|
// per interval.
|
||||||
|
BytesPerInterval int `default:"4096" json:"bytes" yaml:"bytes"`
|
||||||
|
|
||||||
|
// The amount of time that should lapse between data output throttle
|
||||||
|
// checks. This should be defined in milliseconds.
|
||||||
|
CheckInterval int `default:"100" yaml:"check_interval"`
|
||||||
|
}
|
||||||
|
|
||||||
// The location where the panel is running that this daemon should connect to
|
// The location where the panel is running that this daemon should connect to
|
||||||
// to collect data and send events.
|
// to collect data and send events.
|
||||||
PanelLocation string `json:"remote" yaml:"remote"`
|
PanelLocation string `json:"remote" yaml:"remote"`
|
||||||
|
|
||||||
// AllowedMounts is a list of allowed host-system mount points.
|
// AllowedMounts .
|
||||||
// This is required to have the "Server Mounts" feature work properly.
|
|
||||||
AllowedMounts []string `json:"allowed_mounts" yaml:"allowed_mounts"`
|
AllowedMounts []string `json:"allowed_mounts" yaml:"allowed_mounts"`
|
||||||
|
|
||||||
// AllowedOrigins is a list of allowed request origins.
|
|
||||||
// The Panel URL is automatically allowed, this is only needed for adding
|
|
||||||
// additional origins.
|
|
||||||
AllowedOrigins []string `json:"allowed_origins" yaml:"allowed_origins"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Defines the configuration of the internal SFTP server.
|
// Defines the configuration of the internal SFTP server.
|
||||||
@@ -134,7 +148,7 @@ func ReadConfiguration(path string) (*Configuration, error) {
|
|||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var mu sync.RWMutex
|
var Mutex sync.RWMutex
|
||||||
|
|
||||||
var _config *Configuration
|
var _config *Configuration
|
||||||
var _jwtAlgo *jwt.HMACSHA
|
var _jwtAlgo *jwt.HMACSHA
|
||||||
@@ -144,14 +158,14 @@ var _debugViaFlag bool
|
|||||||
// anything trying to set a different configuration value, or read the configuration
|
// anything trying to set a different configuration value, or read the configuration
|
||||||
// will be paused until it is complete.
|
// will be paused until it is complete.
|
||||||
func Set(c *Configuration) {
|
func Set(c *Configuration) {
|
||||||
mu.Lock()
|
Mutex.Lock()
|
||||||
|
|
||||||
if _config == nil || _config.AuthenticationToken != c.AuthenticationToken {
|
if _config == nil || _config.AuthenticationToken != c.AuthenticationToken {
|
||||||
_jwtAlgo = jwt.NewHS256([]byte(c.AuthenticationToken))
|
_jwtAlgo = jwt.NewHS256([]byte(c.AuthenticationToken))
|
||||||
}
|
}
|
||||||
|
|
||||||
_config = c
|
_config = c
|
||||||
mu.Unlock()
|
Mutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetDebugViaFlag(d bool) {
|
func SetDebugViaFlag(d bool) {
|
||||||
@@ -161,16 +175,16 @@ func SetDebugViaFlag(d bool) {
|
|||||||
// Get the global configuration instance. This is a read-safe operation that will block
|
// Get the global configuration instance. This is a read-safe operation that will block
|
||||||
// if the configuration is presently being modified.
|
// if the configuration is presently being modified.
|
||||||
func Get() *Configuration {
|
func Get() *Configuration {
|
||||||
mu.RLock()
|
Mutex.RLock()
|
||||||
defer mu.RUnlock()
|
defer Mutex.RUnlock()
|
||||||
|
|
||||||
return _config
|
return _config
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the in-memory JWT algorithm.
|
// Returns the in-memory JWT algorithm.
|
||||||
func GetJwtAlgorithm() *jwt.HMACSHA {
|
func GetJwtAlgorithm() *jwt.HMACSHA {
|
||||||
mu.RLock()
|
Mutex.RLock()
|
||||||
defer mu.RUnlock()
|
defer Mutex.RUnlock()
|
||||||
|
|
||||||
return _jwtAlgo
|
return _jwtAlgo
|
||||||
}
|
}
|
||||||
@@ -179,7 +193,7 @@ func GetJwtAlgorithm() *jwt.HMACSHA {
|
|||||||
func NewFromPath(path string) (*Configuration, error) {
|
func NewFromPath(path string) (*Configuration, error) {
|
||||||
c := new(Configuration)
|
c := new(Configuration)
|
||||||
if err := defaults.Set(c); err != nil {
|
if err := defaults.Set(c); err != nil {
|
||||||
return c, errors.WithStack(err)
|
return c, err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.unsafeSetPath(path)
|
c.unsafeSetPath(path)
|
||||||
@@ -188,7 +202,7 @@ func NewFromPath(path string) (*Configuration, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sets the path where the configuration file is located on the server. This function should
|
// Sets the path where the configuration file is located on the server. This function should
|
||||||
// not be called except by processes that are generating the configuration such as the configuration
|
// not be called except by processes that are generating the configuration such as the configration
|
||||||
// command shipped with this software.
|
// command shipped with this software.
|
||||||
func (c *Configuration) unsafeSetPath(path string) {
|
func (c *Configuration) unsafeSetPath(path string) {
|
||||||
c.Lock()
|
c.Lock()
|
||||||
@@ -217,12 +231,12 @@ func (c *Configuration) EnsurePterodactylUser() (*user.User, error) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
return u, c.setSystemUser(u)
|
return u, c.setSystemUser(u)
|
||||||
} else if _, ok := err.(user.UnknownUserError); !ok {
|
} else if _, ok := err.(user.UnknownUserError); !ok {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
sysName, err := getSystemName()
|
sysName, err := getSystemName()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var command = fmt.Sprintf("useradd --system --no-create-home --shell /bin/false %s", c.System.Username)
|
var command = fmt.Sprintf("useradd --system --no-create-home --shell /bin/false %s", c.System.Username)
|
||||||
@@ -235,17 +249,17 @@ func (c *Configuration) EnsurePterodactylUser() (*user.User, error) {
|
|||||||
// We have to create the group first on Alpine, so do that here before continuing on
|
// We have to create the group first on Alpine, so do that here before continuing on
|
||||||
// to the user creation process.
|
// to the user creation process.
|
||||||
if _, err := exec.Command("addgroup", "-S", c.System.Username).Output(); err != nil {
|
if _, err := exec.Command("addgroup", "-S", c.System.Username).Output(); err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
split := strings.Split(command, " ")
|
split := strings.Split(command, " ")
|
||||||
if _, err := exec.Command(split[0], split[1:]...).Output(); err != nil {
|
if _, err := exec.Command(split[0], split[1:]...).Output(); err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if u, err := user.Lookup(c.System.Username); err != nil {
|
if u, err := user.Lookup(c.System.Username); err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
return u, c.setSystemUser(u)
|
return u, c.setSystemUser(u)
|
||||||
}
|
}
|
||||||
@@ -266,6 +280,58 @@ func (c *Configuration) setSystemUser(u *user.User) error {
|
|||||||
return c.WriteToDisk()
|
return c.WriteToDisk()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensures that the configured data directory has the correct permissions assigned to
|
||||||
|
// all of the files and folders within.
|
||||||
|
func (c *Configuration) EnsureFilePermissions() error {
|
||||||
|
// Don't run this unless it is configured to be run. On large system this can often slow
|
||||||
|
// things down dramatically during the boot process.
|
||||||
|
if !c.System.SetPermissionsOnBoot {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r := regexp.MustCompile("^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$")
|
||||||
|
|
||||||
|
files, err := ioutil.ReadDir(c.System.Data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
su, err := user.Lookup(c.System.Username)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := new(sync.WaitGroup)
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
// Asynchronously run through the list of files and folders in the data directory. If
|
||||||
|
// the item is not a folder, or is not a folder that matches the expected UUIDv4 format
|
||||||
|
// skip over it.
|
||||||
|
//
|
||||||
|
// If we do have a positive match, run a chown against the directory.
|
||||||
|
go func(f os.FileInfo) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
if !f.IsDir() || !r.MatchString(f.Name()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uid, _ := strconv.Atoi(su.Uid)
|
||||||
|
gid, _ := strconv.Atoi(su.Gid)
|
||||||
|
|
||||||
|
if err := os.Chown(path.Join(c.System.Data, f.Name()), uid, gid); err != nil {
|
||||||
|
log.WithField("error", err).WithField("directory", f.Name()).Warn("failed to chown server directory")
|
||||||
|
}
|
||||||
|
}(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Writes the configuration to the disk as a blocking operation by obtaining an exclusive
|
// Writes the configuration to the disk as a blocking operation by obtaining an exclusive
|
||||||
// lock on the file. This prevents something else from writing at the exact same time and
|
// lock on the file. This prevents something else from writing at the exact same time and
|
||||||
// leading to bad data conditions.
|
// leading to bad data conditions.
|
||||||
@@ -287,11 +353,11 @@ func (c *Configuration) WriteToDisk() error {
|
|||||||
|
|
||||||
b, err := yaml.Marshal(&ccopy)
|
b, err := yaml.Marshal(&ccopy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ioutil.WriteFile(c.GetPath(), b, 0644); err != nil {
|
if err := ioutil.WriteFile(c.GetPath(), b, 0644); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -301,7 +367,7 @@ func (c *Configuration) WriteToDisk() error {
|
|||||||
func getSystemName() (string, error) {
|
func getSystemName() (string, error) {
|
||||||
// use osrelease to get release version and ID
|
// use osrelease to get release version and ID
|
||||||
if release, err := osrelease.Read(); err != nil {
|
if release, err := osrelease.Read(); err != nil {
|
||||||
return "", errors.WithStack(err)
|
return "", err
|
||||||
} else {
|
} else {
|
||||||
return release["ID"], nil
|
return release["ID"], nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type dockerNetworkInterfaces struct {
|
type dockerNetworkInterfaces struct {
|
||||||
V4 struct {
|
V4 struct {
|
||||||
Subnet string `default:"172.18.0.0/16"`
|
Subnet string `default:"172.18.0.0/16"`
|
||||||
@@ -60,33 +53,4 @@ type DockerConfiguration struct {
|
|||||||
// Defines the location of the timezone file on the host system that should
|
// Defines the location of the timezone file on the host system that should
|
||||||
// be mounted into the created containers so that they all use the same time.
|
// be mounted into the created containers so that they all use the same time.
|
||||||
TimezonePath string `default:"/etc/timezone" json:"timezone_path" yaml:"timezone_path"`
|
TimezonePath string `default:"/etc/timezone" json:"timezone_path" yaml:"timezone_path"`
|
||||||
|
|
||||||
// Registries .
|
|
||||||
Registries map[string]RegistryConfiguration `json:"registries" yaml:"registries"`
|
|
||||||
|
|
||||||
// The size of the /tmp directory when mounted into a container. Please be aware that Docker
|
|
||||||
// utilizes host memory for this value, and that we do not keep track of the space used here
|
|
||||||
// so avoid allocating too much to a server.
|
|
||||||
TmpfsSize uint `default:"100" json:"tmpfs_size" yaml:"tmpfs_size"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegistryConfiguration .
|
|
||||||
type RegistryConfiguration struct {
|
|
||||||
Username string `yaml:"username"`
|
|
||||||
Password string `yaml:"password"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Base64 .
|
|
||||||
func (c RegistryConfiguration) Base64() (string, error) {
|
|
||||||
authConfig := types.AuthConfig{
|
|
||||||
Username: c.Username,
|
|
||||||
Password: c.Password,
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := json.Marshal(authConfig)
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return base64.URLEncoding.EncodeToString(b), nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,8 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"html/template"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Defines basic system configuration settings.
|
// Defines basic system configuration settings.
|
||||||
@@ -36,21 +33,24 @@ type SystemConfiguration struct {
|
|||||||
Gid int
|
Gid int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determines if permissions for a server should be set automatically on
|
||||||
|
// daemon boot. This can take a long time on systems with many servers, or on
|
||||||
|
// systems with servers containing thousands of files.
|
||||||
|
//
|
||||||
|
// Setting this to true by default helps us avoid a lot of support requests
|
||||||
|
// from people that keep trying to move files around as a root user leading
|
||||||
|
// to server permission issues.
|
||||||
|
//
|
||||||
|
// In production and heavy use environments where boot speed is essential,
|
||||||
|
// this should be set to false as servers will self-correct permissions on
|
||||||
|
// boot anyways.
|
||||||
|
SetPermissionsOnBoot bool `default:"true" yaml:"set_permissions_on_boot"`
|
||||||
|
|
||||||
// Determines if Wings should detect a server that stops with a normal exit code of
|
// Determines if Wings should detect a server that stops with a normal exit code of
|
||||||
// "0" as being crashed if the process stopped without any Wings interaction. E.g.
|
// "0" as being crashed if the process stopped without any Wings interaction. E.g.
|
||||||
// the user did not press the stop button, but the process stopped cleanly.
|
// the user did not press the stop button, but the process stopped cleanly.
|
||||||
DetectCleanExitAsCrash bool `default:"true" yaml:"detect_clean_exit_as_crash"`
|
DetectCleanExitAsCrash bool `default:"true" yaml:"detect_clean_exit_as_crash"`
|
||||||
|
|
||||||
// If set to true, file permissions for a server will be checked when the process is
|
|
||||||
// booted. This can cause boot delays if the server has a large amount of files. In most
|
|
||||||
// cases disabling this should not have any major impact unless external processes are
|
|
||||||
// frequently modifying a servers' files.
|
|
||||||
CheckPermissionsOnBoot bool `default:"true" yaml:"check_permissions_on_boot"`
|
|
||||||
|
|
||||||
// If set to false Wings will not attempt to write a log rotate configuration to the disk
|
|
||||||
// when it boots and one is not detected.
|
|
||||||
EnableLogRotate bool `default:"true" yaml:"enable_log_rotate"`
|
|
||||||
|
|
||||||
Sftp SftpConfiguration `yaml:"sftp"`
|
Sftp SftpConfiguration `yaml:"sftp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,20 +62,9 @@ func (sc *SystemConfiguration) ConfigureDirectories() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// There are a non-trivial number of users out there whose data directories are actually a
|
log.WithField("path", sc.LogDirectory).Debug("ensuring log directory exists")
|
||||||
// symlink to another location on the disk. If we do not resolve that final destination at this
|
if err := os.MkdirAll(path.Join(sc.LogDirectory, "/install"), 0700); err != nil {
|
||||||
// point things will appear to work, but endless errors will be encountered when we try to
|
return err
|
||||||
// verify accessed paths since they will all end up resolving outside the expected data directory.
|
|
||||||
//
|
|
||||||
// For the sake of automating away as much of this as possible, see if the data directory is a
|
|
||||||
// symlink, and if so resolve to its final real path, and then update the configuration to use
|
|
||||||
// that.
|
|
||||||
if d, err := filepath.EvalSymlinks(sc.Data); err != nil {
|
|
||||||
if !os.IsNotExist(err) {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
} else if d != sc.Data {
|
|
||||||
sc.Data = d
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.WithField("path", sc.Data).Debug("ensuring server data directory exists")
|
log.WithField("path", sc.Data).Debug("ensuring server data directory exists")
|
||||||
@@ -96,47 +85,6 @@ func (sc *SystemConfiguration) ConfigureDirectories() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Writes a logrotate file for wings to the system logrotate configuration directory if one
|
|
||||||
// exists and a logrotate file is not found. This allows us to basically automate away the log
|
|
||||||
// rotation for most installs, but also enable users to make modifications on their own.
|
|
||||||
func (sc *SystemConfiguration) EnableLogRotation() error {
|
|
||||||
// Do nothing if not enabled.
|
|
||||||
if sc.EnableLogRotate == false {
|
|
||||||
log.Info("skipping log rotate configuration, disabled in wings config file")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if st, err := os.Stat("/etc/logrotate.d"); err != nil && !os.IsNotExist(err) {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
} else if (err != nil && os.IsNotExist(err)) || !st.IsDir() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := os.Stat("/etc/logrotate.d/wings"); err != nil && !os.IsNotExist(err) {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
} else if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("no log rotation configuration found, system is configured to support it, adding file now")
|
|
||||||
// If we've gotten to this point it means the logrotate directory exists on the system
|
|
||||||
// but there is not a file for wings already. In that case, let us write a new file to
|
|
||||||
// it so files can be rotated easily.
|
|
||||||
f, err := os.Create("/etc/logrotate.d/wings")
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
t, err := template.ParseFiles("templates/logrotate.tpl")
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.Wrap(t.Execute(f, sc), "failed to write logrotate file to disk")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the location of the JSON file that tracks server states.
|
// Returns the location of the JSON file that tracks server states.
|
||||||
func (sc *SystemConfiguration) GetStatesPath() string {
|
func (sc *SystemConfiguration) GetStatesPath() string {
|
||||||
return path.Join(sc.RootDirectory, "states.json")
|
return path.Join(sc.RootDirectory, "states.json")
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
type ConsoleThrottles struct {
|
|
||||||
// Whether or not the throttler is enabled for this instance.
|
|
||||||
Enabled bool `json:"enabled" yaml:"enabled" default:"true"`
|
|
||||||
|
|
||||||
// The total number of throttle activations that must accumulate before a server is
|
|
||||||
// forcibly stopped for violating these limits.
|
|
||||||
KillAtCount uint64 `json:"kill_at_count" yaml:"kill_at_count" default:"5"`
|
|
||||||
|
|
||||||
// The amount of time in milliseconds that a server process must go through without
|
|
||||||
// triggering an output warning before the throttle activation count begins decreasing.
|
|
||||||
// This time is measured in milliseconds.
|
|
||||||
Decay uint64 `json:"decay" yaml:"decay" default:"10000"`
|
|
||||||
|
|
||||||
// The total number of lines that can be output in a given CheckInterval period before
|
|
||||||
// a warning is triggered and counted against the server.
|
|
||||||
Lines uint64 `json:"lines" yaml:"lines" default:"1000"`
|
|
||||||
|
|
||||||
// The amount of time that must pass between intervals before the count is reset. This
|
|
||||||
// value is in milliseconds.
|
|
||||||
CheckInterval uint64 `json:"check_interval" yaml:"check_interval" default:"100"`
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
package environment
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/docker/go-connections/nat"
|
|
||||||
"github.com/pterodactyl/wings/config"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Defines the allocations available for a given server. When using the Docker environment
|
|
||||||
// driver these correspond to mappings for the container that allow external connections.
|
|
||||||
type Allocations struct {
|
|
||||||
// Defines the default allocation that should be used for this server. This is
|
|
||||||
// what will be used for {SERVER_IP} and {SERVER_PORT} when modifying configuration
|
|
||||||
// files or the startup arguments for a server.
|
|
||||||
DefaultMapping struct {
|
|
||||||
Ip string `json:"ip"`
|
|
||||||
Port int `json:"port"`
|
|
||||||
} `json:"default"`
|
|
||||||
|
|
||||||
// Mappings contains all of the ports that should be assigned to a given server
|
|
||||||
// attached to the IP they correspond to.
|
|
||||||
Mappings map[string][]int `json:"mappings"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Converts the server allocation mappings into a format that can be understood by Docker. While
|
|
||||||
// we do strive to support multiple environments, using Docker's standardized format for the
|
|
||||||
// bindings certainly makes life a little easier for managing things.
|
|
||||||
//
|
|
||||||
// You'll want to use DockerBindings() if you need to re-map 127.0.0.1 to the Docker interface.
|
|
||||||
func (a *Allocations) Bindings() nat.PortMap {
|
|
||||||
var out = nat.PortMap{}
|
|
||||||
|
|
||||||
for ip, ports := range a.Mappings {
|
|
||||||
for _, port := range ports {
|
|
||||||
// Skip over invalid ports.
|
|
||||||
if port < 1 || port > 65535 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
binding := []nat.PortBinding{
|
|
||||||
{
|
|
||||||
HostIP: ip,
|
|
||||||
HostPort: strconv.Itoa(port),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
out[nat.Port(fmt.Sprintf("%d/tcp", port))] = binding
|
|
||||||
out[nat.Port(fmt.Sprintf("%d/udp", port))] = binding
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the bindings for the server in a way that is supported correctly by Docker. This replaces
|
|
||||||
// any reference to 127.0.0.1 with the IP of the pterodactyl0 network interface which will allow the
|
|
||||||
// server to operate on a local address while still being accessible by other containers.
|
|
||||||
func (a *Allocations) DockerBindings() nat.PortMap {
|
|
||||||
iface := config.Get().Docker.Network.Interface
|
|
||||||
|
|
||||||
out := a.Bindings()
|
|
||||||
// Loop over all of the bindings for this container, and convert any that reference 127.0.0.1
|
|
||||||
// to use the pterodactyl0 network interface IP, as that is the true local for what people are
|
|
||||||
// trying to do when creating servers.
|
|
||||||
for p, binds := range out {
|
|
||||||
for i, alloc := range binds {
|
|
||||||
if alloc.HostIP != "127.0.0.1" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// If using ISPN just delete the local allocation from the server.
|
|
||||||
if config.Get().Docker.Network.ISPN {
|
|
||||||
out[p] = append(out[p][:i], out[p][i+1:]...)
|
|
||||||
} else {
|
|
||||||
out[p][i] = nat.PortBinding{
|
|
||||||
HostIP: iface,
|
|
||||||
HostPort: alloc.HostPort,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// Converts the server allocation mappings into a PortSet that can be understood
|
|
||||||
// by Docker. This formatting is slightly different than "Bindings" as it should
|
|
||||||
// return an empty struct rather than a binding.
|
|
||||||
//
|
|
||||||
// To accomplish this, we'll just get the values from "DockerBindings" and then set them
|
|
||||||
// to empty structs. Because why not.
|
|
||||||
func (a *Allocations) Exposed() nat.PortSet {
|
|
||||||
var out = nat.PortSet{}
|
|
||||||
|
|
||||||
for port := range a.DockerBindings() {
|
|
||||||
out[port] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
package environment
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Settings struct {
|
|
||||||
Mounts []Mount
|
|
||||||
Allocations Allocations
|
|
||||||
Limits Limits
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defines the actual configuration struct for the environment with all of the settings
|
|
||||||
// defined within it.
|
|
||||||
type Configuration struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
|
|
||||||
environmentVariables []string
|
|
||||||
settings Settings
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a new environment configuration with the given settings and environment variables
|
|
||||||
// defined within it.
|
|
||||||
func NewConfiguration(s Settings, envVars []string) *Configuration {
|
|
||||||
return &Configuration{
|
|
||||||
environmentVariables: envVars,
|
|
||||||
settings: s,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Updates the settings struct for this environment on the fly. This allows modified servers to
|
|
||||||
// automatically push those changes to the environment.
|
|
||||||
func (c *Configuration) SetSettings(s Settings) {
|
|
||||||
c.mu.Lock()
|
|
||||||
c.settings = s
|
|
||||||
c.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Updates the environment variables associated with this environment by replacing the entire
|
|
||||||
// array of them with a new one.
|
|
||||||
func (c *Configuration) SetEnvironmentVariables(ev []string) {
|
|
||||||
c.mu.Lock()
|
|
||||||
c.environmentVariables = ev
|
|
||||||
c.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the limits assigned to this environment.
|
|
||||||
func (c *Configuration) Limits() Limits {
|
|
||||||
c.mu.RLock()
|
|
||||||
defer c.mu.RUnlock()
|
|
||||||
|
|
||||||
return c.settings.Limits
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the allocations associated with this environment.
|
|
||||||
func (c *Configuration) Allocations() Allocations {
|
|
||||||
c.mu.RLock()
|
|
||||||
defer c.mu.RUnlock()
|
|
||||||
|
|
||||||
return c.settings.Allocations
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns all of the mounts associated with this environment.
|
|
||||||
func (c *Configuration) Mounts() []Mount {
|
|
||||||
c.mu.RLock()
|
|
||||||
defer c.mu.RUnlock()
|
|
||||||
|
|
||||||
return c.settings.Mounts
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the environment variables associated with this instance.
|
|
||||||
func (c *Configuration) EnvironmentVariables() []string {
|
|
||||||
c.mu.RLock()
|
|
||||||
defer c.mu.RUnlock()
|
|
||||||
|
|
||||||
return c.environmentVariables
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package docker
|
|
||||||
|
|
||||||
import "io"
|
|
||||||
|
|
||||||
type Console struct {
|
|
||||||
HandlerFunc *func(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ io.Writer = Console{}
|
|
||||||
|
|
||||||
func (c Console) Write(b []byte) (int, error) {
|
|
||||||
if c.HandlerFunc != nil {
|
|
||||||
l := make([]byte, len(b))
|
|
||||||
copy(l, b)
|
|
||||||
|
|
||||||
(*c.HandlerFunc)(string(l))
|
|
||||||
}
|
|
||||||
|
|
||||||
return len(b), nil
|
|
||||||
}
|
|
||||||
@@ -1,385 +0,0 @@
|
|||||||
package docker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"github.com/apex/log"
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
"github.com/docker/docker/api/types/container"
|
|
||||||
"github.com/docker/docker/api/types/mount"
|
|
||||||
"github.com/docker/docker/client"
|
|
||||||
"github.com/docker/docker/daemon/logger/jsonfilelog"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/pterodactyl/wings/config"
|
|
||||||
"github.com/pterodactyl/wings/environment"
|
|
||||||
"io"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Attaches to the docker container itself and ensures that we can pipe data in and out
|
|
||||||
// of the process stream. This should not be used for reading console data as you *will*
|
|
||||||
// miss important output at the beginning because of the time delay with attaching to the
|
|
||||||
// output.
|
|
||||||
func (e *Environment) Attach() error {
|
|
||||||
if e.IsAttached() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := e.followOutput(); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := types.ContainerAttachOptions{
|
|
||||||
Stdin: true,
|
|
||||||
Stdout: true,
|
|
||||||
Stderr: true,
|
|
||||||
Stream: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the stream again with the container.
|
|
||||||
if st, err := e.client.ContainerAttach(context.Background(), e.Id, opts); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
} else {
|
|
||||||
e.SetStream(&st)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := new(Console)
|
|
||||||
go func(console *Console) {
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
|
|
||||||
defer cancel()
|
|
||||||
defer e.stream.Close()
|
|
||||||
defer func() {
|
|
||||||
e.setState(environment.ProcessOfflineState)
|
|
||||||
e.SetStream(nil)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Poll resources in a separate thread since this will block the copy call below
|
|
||||||
// from being reached until it is completed if not run in a separate process. However,
|
|
||||||
// we still want it to be stopped when the copy operation below is finished running which
|
|
||||||
// indicates that the container is no longer running.
|
|
||||||
go func(ctx context.Context) {
|
|
||||||
if err := e.pollResources(ctx); err != nil {
|
|
||||||
log.WithField("environment_id", e.Id).WithField("error", errors.WithStack(err)).Error("error during environment resource polling")
|
|
||||||
}
|
|
||||||
}(ctx)
|
|
||||||
|
|
||||||
// Stream the reader output to the console which will then fire off events and handle console
|
|
||||||
// throttling and sending the output to the user.
|
|
||||||
if _, err := io.Copy(console, e.stream.Reader); err != nil {
|
|
||||||
log.WithField("environment_id", e.Id).WithField("error", errors.WithStack(err)).Error("error while copying environment output to console")
|
|
||||||
}
|
|
||||||
}(c)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Environment) resources() container.Resources {
|
|
||||||
l := e.Configuration.Limits()
|
|
||||||
|
|
||||||
return container.Resources{
|
|
||||||
Memory: l.BoundedMemoryLimit(),
|
|
||||||
MemoryReservation: l.MemoryLimit * 1_000_000,
|
|
||||||
MemorySwap: l.ConvertedSwap(),
|
|
||||||
CPUQuota: l.ConvertedCpuLimit(),
|
|
||||||
CPUPeriod: 100_000,
|
|
||||||
CPUShares: 1024,
|
|
||||||
BlkioWeight: l.IoWeight,
|
|
||||||
OomKillDisable: &l.OOMDisabled,
|
|
||||||
CpusetCpus: l.Threads,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Performs an in-place update of the Docker container's resource limits without actually
|
|
||||||
// making any changes to the operational state of the container. This allows memory, cpu,
|
|
||||||
// and IO limitations to be adjusted on the fly for individual instances.
|
|
||||||
func (e *Environment) InSituUpdate() error {
|
|
||||||
if _, err := e.client.ContainerInspect(context.Background(), e.Id); err != nil {
|
|
||||||
// If the container doesn't exist for some reason there really isn't anything
|
|
||||||
// we can do to fix that in this process (it doesn't make sense at least). In those
|
|
||||||
// cases just return without doing anything since we still want to save the configuration
|
|
||||||
// to the disk.
|
|
||||||
//
|
|
||||||
// We'll let a boot process make modifications to the container if needed at this point.
|
|
||||||
if client.IsErrNotFound(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
u := container.UpdateConfig{
|
|
||||||
Resources: e.resources(),
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
|
||||||
defer cancel()
|
|
||||||
if _, err := e.client.ContainerUpdate(ctx, e.Id, u); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a new container for the server using all of the data that is currently
|
|
||||||
// available for it. If the container already exists it will be returnee.
|
|
||||||
func (e *Environment) Create() error {
|
|
||||||
// If the container already exists don't hit the user with an error, just return
|
|
||||||
// the current information about it which is what we would do when creating the
|
|
||||||
// container anyways.
|
|
||||||
if _, err := e.client.ContainerInspect(context.Background(), e.Id); err == nil {
|
|
||||||
return nil
|
|
||||||
} else if !client.IsErrNotFound(err) {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to pull the requested image before creating the container.
|
|
||||||
if err := e.ensureImageExists(e.meta.Image); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
a := e.Configuration.Allocations()
|
|
||||||
|
|
||||||
evs := e.Configuration.EnvironmentVariables()
|
|
||||||
for i, v := range evs {
|
|
||||||
// Convert 127.0.0.1 to the pterodactyl0 network interface if the environment is Docker
|
|
||||||
// so that the server operates as expected.
|
|
||||||
if v == "SERVER_IP=127.0.0.1" {
|
|
||||||
evs[i] = "SERVER_IP="+config.Get().Docker.Network.Interface
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
conf := &container.Config{
|
|
||||||
Hostname: e.Id,
|
|
||||||
Domainname: config.Get().Docker.Domainname,
|
|
||||||
User: strconv.Itoa(config.Get().System.User.Uid),
|
|
||||||
AttachStdin: true,
|
|
||||||
AttachStdout: true,
|
|
||||||
AttachStderr: true,
|
|
||||||
OpenStdin: true,
|
|
||||||
Tty: true,
|
|
||||||
ExposedPorts: a.Exposed(),
|
|
||||||
Image: e.meta.Image,
|
|
||||||
Env: e.Configuration.EnvironmentVariables(),
|
|
||||||
Labels: map[string]string{
|
|
||||||
"Service": "Pterodactyl",
|
|
||||||
"ContainerType": "server_process",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpfsSize := strconv.Itoa(int(config.Get().Docker.TmpfsSize))
|
|
||||||
|
|
||||||
hostConf := &container.HostConfig{
|
|
||||||
PortBindings: a.DockerBindings(),
|
|
||||||
|
|
||||||
// Configure the mounts for this container. First mount the server data directory
|
|
||||||
// into the container as a r/w bind.
|
|
||||||
Mounts: e.convertMounts(),
|
|
||||||
|
|
||||||
// Configure the /tmp folder mapping in containers. This is necessary for some
|
|
||||||
// games that need to make use of it for downloads and other installation processes.
|
|
||||||
Tmpfs: map[string]string{
|
|
||||||
"/tmp": "rw,exec,nosuid,size=" + tmpfsSize + "M",
|
|
||||||
},
|
|
||||||
|
|
||||||
// Define resource limits for the container based on the data passed through
|
|
||||||
// from the Panel.
|
|
||||||
Resources: e.resources(),
|
|
||||||
|
|
||||||
DNS: config.Get().Docker.Network.Dns,
|
|
||||||
|
|
||||||
// Configure logging for the container to make it easier on the Daemon to grab
|
|
||||||
// the server output. Ensure that we don't use too much space on the host machine
|
|
||||||
// since we only need it for the last few hundred lines of output and don't care
|
|
||||||
// about anything else in it.
|
|
||||||
LogConfig: container.LogConfig{
|
|
||||||
Type: jsonfilelog.Name,
|
|
||||||
Config: map[string]string{
|
|
||||||
"max-size": "5m",
|
|
||||||
"max-file": "1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
SecurityOpt: []string{"no-new-privileges"},
|
|
||||||
ReadonlyRootfs: true,
|
|
||||||
CapDrop: []string{
|
|
||||||
"setpcap", "mknod", "audit_write", "net_raw", "dac_override",
|
|
||||||
"fowner", "fsetid", "net_bind_service", "sys_chroot", "setfcap",
|
|
||||||
},
|
|
||||||
NetworkMode: container.NetworkMode(config.Get().Docker.Network.Mode),
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := e.client.ContainerCreate(context.Background(), conf, hostConf, nil, e.Id); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Environment) convertMounts() []mount.Mount {
|
|
||||||
var out []mount.Mount
|
|
||||||
|
|
||||||
for _, m := range e.Configuration.Mounts() {
|
|
||||||
out = append(out, mount.Mount{
|
|
||||||
Type: mount.TypeBind,
|
|
||||||
Source: m.Source,
|
|
||||||
Target: m.Target,
|
|
||||||
ReadOnly: m.ReadOnly,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the Docker container from the machine. If the container is currently running
|
|
||||||
// it will be forcibly stopped by Docker.
|
|
||||||
func (e *Environment) Destroy() error {
|
|
||||||
// We set it to stopping than offline to prevent crash detection from being triggered.
|
|
||||||
e.setState(environment.ProcessStoppingState)
|
|
||||||
|
|
||||||
err := e.client.ContainerRemove(context.Background(), e.Id, types.ContainerRemoveOptions{
|
|
||||||
RemoveVolumes: true,
|
|
||||||
RemoveLinks: false,
|
|
||||||
Force: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Don't trigger a destroy failure if we try to delete a container that does not
|
|
||||||
// exist on the system. We're just a step ahead of ourselves in that case.
|
|
||||||
//
|
|
||||||
// @see https://github.com/pterodactyl/panel/issues/2001
|
|
||||||
if err != nil && client.IsErrNotFound(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
e.setState(environment.ProcessOfflineState)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attaches to the log for the container. This avoids us missing crucial output that
|
|
||||||
// happens in the split seconds before the code moves from 'Starting' to 'Attaching'
|
|
||||||
// on the process.
|
|
||||||
func (e *Environment) followOutput() error {
|
|
||||||
if exists, err := e.Exists(); !exists {
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.New(fmt.Sprintf("no such container: %s", e.Id))
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := types.ContainerLogsOptions{
|
|
||||||
ShowStderr: true,
|
|
||||||
ShowStdout: true,
|
|
||||||
Follow: true,
|
|
||||||
Since: time.Now().Format(time.RFC3339),
|
|
||||||
}
|
|
||||||
|
|
||||||
reader, err := e.client.ContainerLogs(context.Background(), e.Id, opts)
|
|
||||||
|
|
||||||
go func(r io.ReadCloser) {
|
|
||||||
defer r.Close()
|
|
||||||
|
|
||||||
s := bufio.NewScanner(r)
|
|
||||||
for s.Scan() {
|
|
||||||
e.Events().Publish(environment.ConsoleOutputEvent, s.Text())
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.Err(); err != nil {
|
|
||||||
log.WithField("error", err).WithField("container_id", e.Id).Warn("error processing scanner line in console output")
|
|
||||||
}
|
|
||||||
}(reader)
|
|
||||||
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pulls the image from Docker. If there is an error while pulling the image from the source
|
|
||||||
// but the image already exists locally, we will report that error to the logger but continue
|
|
||||||
// with the process.
|
|
||||||
//
|
|
||||||
// The reasoning behind this is that Quay has had some serious outages as of late, and we don't
|
|
||||||
// need to block all of the servers from booting just because of that. I'd imagine in a lot of
|
|
||||||
// cases an outage shouldn't affect users too badly. It'll at least keep existing servers working
|
|
||||||
// correctly if anything.
|
|
||||||
//
|
|
||||||
// TODO: local images
|
|
||||||
func (e *Environment) ensureImageExists(image string) error {
|
|
||||||
// Give it up to 15 minutes to pull the image. I think this should cover 99.8% of cases where an
|
|
||||||
// image pull might fail. I can't imagine it will ever take more than 15 minutes to fully pull
|
|
||||||
// an image. Let me know when I am inevitably wrong here...
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*15)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// Get a registry auth configuration from the config.
|
|
||||||
var registryAuth *config.RegistryConfiguration
|
|
||||||
for registry, c := range config.Get().Docker.Registries {
|
|
||||||
if !strings.HasPrefix(image, registry) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
log.WithField("registry", registry).Debug("using authentication for registry")
|
|
||||||
registryAuth = &c
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the ImagePullOptions.
|
|
||||||
imagePullOptions := types.ImagePullOptions{All: false}
|
|
||||||
if registryAuth != nil {
|
|
||||||
b64, err := registryAuth.Base64()
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("failed to get registry auth credentials")
|
|
||||||
}
|
|
||||||
|
|
||||||
// b64 is a string so if there is an error it will just be empty, not nil.
|
|
||||||
imagePullOptions.RegistryAuth = b64
|
|
||||||
}
|
|
||||||
|
|
||||||
out, err := e.client.ImagePull(ctx, image, imagePullOptions)
|
|
||||||
if err != nil {
|
|
||||||
images, ierr := e.client.ImageList(ctx, types.ImageListOptions{})
|
|
||||||
if ierr != nil {
|
|
||||||
// Well damn, something has gone really wrong here, just go ahead and abort there
|
|
||||||
// isn't much anything we can do to try and self-recover from this.
|
|
||||||
return ierr
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, img := range images {
|
|
||||||
for _, t := range img.RepoTags {
|
|
||||||
if t != image {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"image": image,
|
|
||||||
"container_id": e.Id,
|
|
||||||
"error": errors.New(err.Error()),
|
|
||||||
}).Warn("unable to pull requested image from remote source, however the image exists locally")
|
|
||||||
|
|
||||||
// Okay, we found a matching container image, in that case just go ahead and return
|
|
||||||
// from this function, since there is nothing else we need to do here.
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer out.Close()
|
|
||||||
|
|
||||||
log.WithField("image", image).Debug("pulling docker image... this could take a bit of time")
|
|
||||||
|
|
||||||
// I'm not sure what the best approach here is, but this will block execution until the image
|
|
||||||
// is done being pulled, which is what we need.
|
|
||||||
scanner := bufio.NewScanner(out)
|
|
||||||
for scanner.Scan() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
package docker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
"github.com/docker/docker/client"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/pterodactyl/wings/api"
|
|
||||||
"github.com/pterodactyl/wings/environment"
|
|
||||||
"github.com/pterodactyl/wings/events"
|
|
||||||
"io"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Metadata struct {
|
|
||||||
Image string
|
|
||||||
Stop *api.ProcessStopConfiguration
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that the Docker environment is always implementing all of the methods
|
|
||||||
// from the base environment interface.
|
|
||||||
var _ environment.ProcessEnvironment = (*Environment)(nil)
|
|
||||||
|
|
||||||
type Environment struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
eventMu sync.Mutex
|
|
||||||
|
|
||||||
// The public identifier for this environment. In this case it is the Docker container
|
|
||||||
// name that will be used for all instances created under it.
|
|
||||||
Id string
|
|
||||||
|
|
||||||
// The environment configuration.
|
|
||||||
Configuration *environment.Configuration
|
|
||||||
|
|
||||||
meta *Metadata
|
|
||||||
|
|
||||||
// The Docker client being used for this instance.
|
|
||||||
client *client.Client
|
|
||||||
|
|
||||||
// Controls the hijacked response stream which exists only when we're attached to
|
|
||||||
// the running container instance.
|
|
||||||
stream *types.HijackedResponse
|
|
||||||
|
|
||||||
// Holds the stats stream used by the polling commands so that we can easily close it out.
|
|
||||||
stats io.ReadCloser
|
|
||||||
|
|
||||||
emitter *events.EventBus
|
|
||||||
|
|
||||||
// Tracks the environment state.
|
|
||||||
st string
|
|
||||||
stMu sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a new base Docker environment. The ID passed through will be the ID that is used to
|
|
||||||
// reference the container from here on out. This should be unique per-server (we use the UUID
|
|
||||||
// by default). The container does not need to exist at this point.
|
|
||||||
func New(id string, m *Metadata, c *environment.Configuration) (*Environment, error) {
|
|
||||||
cli, err := client.NewClientWithOpts(client.FromEnv)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
e := &Environment{
|
|
||||||
Id: id,
|
|
||||||
Configuration: c,
|
|
||||||
meta: m,
|
|
||||||
client: cli,
|
|
||||||
}
|
|
||||||
|
|
||||||
return e, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Environment) Type() string {
|
|
||||||
return "docker"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set if this process is currently attached to the process.
|
|
||||||
func (e *Environment) SetStream(s *types.HijackedResponse) {
|
|
||||||
e.mu.Lock()
|
|
||||||
e.stream = s
|
|
||||||
e.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine if the this process is currently attached to the container.
|
|
||||||
func (e *Environment) IsAttached() bool {
|
|
||||||
e.mu.RLock()
|
|
||||||
defer e.mu.RUnlock()
|
|
||||||
|
|
||||||
return e.stream != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Environment) Events() *events.EventBus {
|
|
||||||
e.eventMu.Lock()
|
|
||||||
defer e.eventMu.Unlock()
|
|
||||||
|
|
||||||
if e.emitter == nil {
|
|
||||||
e.emitter = events.New()
|
|
||||||
}
|
|
||||||
|
|
||||||
return e.emitter
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determines if the container exists in this environment. The ID passed through should be the
|
|
||||||
// server UUID since containers are created utilizing the server UUID as the name and docker
|
|
||||||
// will work fine when using the container name as the lookup parameter in addition to the longer
|
|
||||||
// ID auto-assigned when the container is created.
|
|
||||||
func (e *Environment) Exists() (bool, error) {
|
|
||||||
_, err := e.client.ContainerInspect(context.Background(), e.Id)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
// If this error is because the container instance wasn't found via Docker we
|
|
||||||
// can safely ignore the error and just return false.
|
|
||||||
if client.IsErrNotFound(err) {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determines if the server's docker container is currently running. If there is no container
|
|
||||||
// present, an error will be raised (since this shouldn't be a case that ever happens under
|
|
||||||
// correctly developed circumstances).
|
|
||||||
//
|
|
||||||
// You can confirm if the instance wasn't found by using client.IsErrNotFound from the Docker
|
|
||||||
// API.
|
|
||||||
//
|
|
||||||
// @see docker/client/errors.go
|
|
||||||
func (e *Environment) IsRunning() (bool, error) {
|
|
||||||
c, err := e.client.ContainerInspect(context.Background(), e.Id)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.State.Running, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the container exit state and return the exit code and whether or not
|
|
||||||
// the container was killed by the OOM killer.
|
|
||||||
func (e *Environment) ExitState() (uint32, bool, error) {
|
|
||||||
c, err := e.client.ContainerInspect(context.Background(), e.Id)
|
|
||||||
if err != nil {
|
|
||||||
// I'm not entirely sure how this can happen to be honest. I tried deleting a
|
|
||||||
// container _while_ a server was running and wings gracefully saw the crash and
|
|
||||||
// created a new container for it.
|
|
||||||
//
|
|
||||||
// However, someone reported an error in Discord about this scenario happening,
|
|
||||||
// so I guess this should prevent it? They didn't tell me how they caused it though
|
|
||||||
// so that's a mystery that will have to go unsolved.
|
|
||||||
//
|
|
||||||
// @see https://github.com/pterodactyl/panel/issues/2003
|
|
||||||
if client.IsErrNotFound(err) {
|
|
||||||
return 1, false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, false, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return uint32(c.State.ExitCode), c.State.OOMKilled, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the environment configuration allowing a process to make modifications of the
|
|
||||||
// environment on the fly.
|
|
||||||
func (e *Environment) Config() *environment.Configuration {
|
|
||||||
e.mu.RLock()
|
|
||||||
defer e.mu.RUnlock()
|
|
||||||
|
|
||||||
return e.Configuration
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sets the stop configuration for the environment.
|
|
||||||
func (e *Environment) SetStopConfiguration(c *api.ProcessStopConfiguration) {
|
|
||||||
e.mu.Lock()
|
|
||||||
e.meta.Stop = c
|
|
||||||
e.mu.Unlock()
|
|
||||||
}
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
package docker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"github.com/apex/log"
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
"github.com/docker/docker/api/types/container"
|
|
||||||
"github.com/docker/docker/client"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/pterodactyl/wings/api"
|
|
||||||
"github.com/pterodactyl/wings/environment"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Run before the container starts and get the process configuration from the Panel.
|
|
||||||
// This is important since we use this to check configuration files as well as ensure
|
|
||||||
// we always have the latest version of an egg available for server processes.
|
|
||||||
//
|
|
||||||
// This process will also confirm that the server environment exists and is in a bootable
|
|
||||||
// state. This ensures that unexpected container deletion while Wings is running does
|
|
||||||
// not result in the server becoming unbootable.
|
|
||||||
func (e *Environment) OnBeforeStart() error {
|
|
||||||
// Always destroy and re-create the server container to ensure that synced data from
|
|
||||||
// the Panel is usee.
|
|
||||||
if err := e.client.ContainerRemove(context.Background(), e.Id, types.ContainerRemoveOptions{RemoveVolumes: true}); err != nil {
|
|
||||||
if !client.IsErrNotFound(err) {
|
|
||||||
return errors.Wrap(err, "failed to remove server docker container during pre-boot")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The Create() function will check if the container exists in the first place, and if
|
|
||||||
// so just silently return without an error. Otherwise, it will try to create the necessary
|
|
||||||
// container and data storage directory.
|
|
||||||
//
|
|
||||||
// This won't actually run an installation process however, it is just here to ensure the
|
|
||||||
// environment gets created properly if it is missing and the server is started. We're making
|
|
||||||
// an assumption that all of the files will still exist at this point.
|
|
||||||
if err := e.Create(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Starts the server environment and begins piping output to the event listeners for the
|
|
||||||
// console. If a container does not exist, or needs to be rebuilt that will happen in the
|
|
||||||
// call to OnBeforeStart().
|
|
||||||
func (e *Environment) Start() error {
|
|
||||||
sawError := false
|
|
||||||
// If sawError is set to true there was an error somewhere in the pipeline that
|
|
||||||
// got passed up, but we also want to ensure we set the server to be offline at
|
|
||||||
// that point.
|
|
||||||
defer func() {
|
|
||||||
if sawError {
|
|
||||||
// If we don't set it to stopping first, you'll trigger crash detection which
|
|
||||||
// we don't want to do at this point since it'll just immediately try to do the
|
|
||||||
// exact same action that lead to it crashing in the first place...
|
|
||||||
e.setState(environment.ProcessStoppingState)
|
|
||||||
e.setState(environment.ProcessOfflineState)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if c, err := e.client.ContainerInspect(context.Background(), e.Id); err != nil {
|
|
||||||
// Do nothing if the container is not found, we just don't want to continue
|
|
||||||
// to the next block of code here. This check was inlined here to guard against
|
|
||||||
// a nil-pointer when checking c.State below.
|
|
||||||
//
|
|
||||||
// @see https://github.com/pterodactyl/panel/issues/2000
|
|
||||||
if !client.IsErrNotFound(err) {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If the server is running update our internal state and continue on with the attach.
|
|
||||||
if c.State.Running {
|
|
||||||
e.setState(environment.ProcessRunningState)
|
|
||||||
|
|
||||||
return e.Attach()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Truncate the log file so we don't end up outputting a bunch of useless log information
|
|
||||||
// to the websocket and whatnot. Check first that the path and file exist before trying
|
|
||||||
// to truncate them.
|
|
||||||
if _, err := os.Stat(c.LogPath); err == nil {
|
|
||||||
if err := os.Truncate(c.LogPath, 0); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
e.setState(environment.ProcessStartingState)
|
|
||||||
|
|
||||||
// Set this to true for now, we will set it to false once we reach the
|
|
||||||
// end of this chain.
|
|
||||||
sawError = true
|
|
||||||
|
|
||||||
// Run the before start function and wait for it to finish. This will validate that the container
|
|
||||||
// exists on the system, and rebuild the container if that is required for server booting to
|
|
||||||
// occur.
|
|
||||||
if err := e.OnBeforeStart(); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
if err := e.client.ContainerStart(ctx, e.Id, types.ContainerStartOptions{}); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// No errors, good to continue through.
|
|
||||||
sawError = false
|
|
||||||
|
|
||||||
return e.Attach()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stops the container that the server is running in. This will allow up to 30 seconds to pass
|
|
||||||
// before the container is forcefully terminated if we are trying to stop it without using a command
|
|
||||||
// sent into the instance.
|
|
||||||
//
|
|
||||||
// You most likely want to be using WaitForStop() rather than this function, since this will return
|
|
||||||
// as soon as the command is sent, rather than waiting for the process to be completed stopped.
|
|
||||||
func (e *Environment) Stop() error {
|
|
||||||
e.mu.RLock()
|
|
||||||
s := e.meta.Stop
|
|
||||||
e.mu.RUnlock()
|
|
||||||
|
|
||||||
if s == nil || s.Type == api.ProcessStopSignal {
|
|
||||||
if s == nil {
|
|
||||||
log.WithField("container_id", e.Id).Warn("no stop configuration detected for environment, using termination procedure")
|
|
||||||
}
|
|
||||||
|
|
||||||
return e.Terminate(os.Kill)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the process is already offline don't switch it back to stopping. Just leave it how
|
|
||||||
// it is and continue through to the stop handling for the process.
|
|
||||||
if e.State() != environment.ProcessOfflineState {
|
|
||||||
e.setState(environment.ProcessStoppingState)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only attempt to send the stop command to the instance if we are actually attached to
|
|
||||||
// the instance. If we are not for some reason, just send the container stop event.
|
|
||||||
if e.IsAttached() && s.Type == api.ProcessStopCommand {
|
|
||||||
return e.SendCommand(s.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
t := time.Second * 30
|
|
||||||
err := e.client.ContainerStop(context.Background(), e.Id, &t)
|
|
||||||
if err != nil {
|
|
||||||
// If the container does not exist just mark the process as stopped and return without
|
|
||||||
// an error.
|
|
||||||
if client.IsErrNotFound(err) {
|
|
||||||
e.SetStream(nil)
|
|
||||||
e.setState(environment.ProcessOfflineState)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempts to gracefully stop a server using the defined stop command. If the server
|
|
||||||
// does not stop after seconds have passed, an error will be returned, or the instance
|
|
||||||
// will be terminated forcefully depending on the value of the second argument.
|
|
||||||
func (e *Environment) WaitForStop(seconds uint, terminate bool) error {
|
|
||||||
if err := e.Stop(); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(seconds)*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// Block the return of this function until the container as been marked as no
|
|
||||||
// longer running. If this wait does not end by the time seconds have passed,
|
|
||||||
// attempt to terminate the container, or return an error.
|
|
||||||
ok, errChan := e.client.ContainerWait(ctx, e.Id, container.WaitConditionNotRunning)
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
|
||||||
if terminate {
|
|
||||||
return e.Terminate(os.Kill)
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.WithStack(ctxErr)
|
|
||||||
}
|
|
||||||
case err := <-errChan:
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
case <-ok:
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forcefully terminates the container using the signal passed through.
|
|
||||||
func (e *Environment) Terminate(signal os.Signal) error {
|
|
||||||
c, err := e.client.ContainerInspect(context.Background(), e.Id)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !c.State.Running {
|
|
||||||
// If the container is not running but we're not already in a stopped state go ahead
|
|
||||||
// and update things to indicate we should be completely stopped now. Set to stopping
|
|
||||||
// first so crash detection is not triggered.
|
|
||||||
if e.State() != environment.ProcessOfflineState {
|
|
||||||
e.setState(environment.ProcessStoppingState)
|
|
||||||
e.setState(environment.ProcessOfflineState)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// We set it to stopping than offline to prevent crash detection from being triggered.
|
|
||||||
e.setState(environment.ProcessStoppingState)
|
|
||||||
|
|
||||||
sig := strings.TrimSuffix(strings.TrimPrefix(signal.String(), "signal "), "ed")
|
|
||||||
|
|
||||||
if err := e.client.ContainerKill(context.Background(), e.Id, sig); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
e.setState(environment.ProcessOfflineState)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package docker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/pterodactyl/wings/environment"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Returns the current environment state.
|
|
||||||
func (e *Environment) State() string {
|
|
||||||
e.stMu.RLock()
|
|
||||||
defer e.stMu.RUnlock()
|
|
||||||
|
|
||||||
return e.st
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sets the state of the environment. This emits an event that server's can hook into to
|
|
||||||
// take their own actions and track their own state based on the environment.
|
|
||||||
func (e *Environment) setState(state string) error {
|
|
||||||
if state != environment.ProcessOfflineState &&
|
|
||||||
state != environment.ProcessStartingState &&
|
|
||||||
state != environment.ProcessRunningState &&
|
|
||||||
state != environment.ProcessStoppingState {
|
|
||||||
return errors.New(fmt.Sprintf("invalid server state received: %s", state))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the current state of the environment before changing it.
|
|
||||||
prevState := e.State()
|
|
||||||
|
|
||||||
// Emit the event to any listeners that are currently registered.
|
|
||||||
if prevState != state {
|
|
||||||
// If the state changed make sure we update the internal tracking to note that.
|
|
||||||
e.stMu.Lock()
|
|
||||||
e.st = state
|
|
||||||
e.stMu.Unlock()
|
|
||||||
|
|
||||||
e.Events().Publish(environment.StateChangeEvent, e.State())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
package docker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/apex/log"
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/pterodactyl/wings/environment"
|
|
||||||
"io"
|
|
||||||
"math"
|
|
||||||
"sync/atomic"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Attach to the instance and then automatically emit an event whenever the resource usage for the
|
|
||||||
// server process changes.
|
|
||||||
func (e *Environment) pollResources(ctx context.Context) error {
|
|
||||||
l := log.WithField("container_id", e.Id)
|
|
||||||
|
|
||||||
l.Debug("starting resource polling for container")
|
|
||||||
defer l.Debug("stopped resource polling for container")
|
|
||||||
|
|
||||||
if e.State() == environment.ProcessOfflineState {
|
|
||||||
return errors.New("cannot enable resource polling on a stopped server")
|
|
||||||
}
|
|
||||||
|
|
||||||
stats, err := e.client.ContainerStats(context.Background(), e.Id, true)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
defer stats.Body.Close()
|
|
||||||
|
|
||||||
dec := json.NewDecoder(stats.Body)
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
default:
|
|
||||||
var v *types.StatsJSON
|
|
||||||
|
|
||||||
if err := dec.Decode(&v); err != nil {
|
|
||||||
if err != io.EOF {
|
|
||||||
l.WithField("error", errors.WithStack(err)).Warn("error while processing Docker stats output for container")
|
|
||||||
} else {
|
|
||||||
l.Debug("io.EOF encountered during stats decode, stopping polling...")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable collection if the server is in an offline state and this process is still running.
|
|
||||||
if e.State() == environment.ProcessOfflineState {
|
|
||||||
l.Debug("process in offline state while resource polling is still active; stopping poll")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var rx uint64
|
|
||||||
var tx uint64
|
|
||||||
for _, nw := range v.Networks {
|
|
||||||
atomic.AddUint64(&rx, nw.RxBytes)
|
|
||||||
atomic.AddUint64(&tx, nw.RxBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
st := &environment.Stats{
|
|
||||||
Memory: calculateDockerMemory(v.MemoryStats),
|
|
||||||
MemoryLimit: v.MemoryStats.Limit,
|
|
||||||
CpuAbsolute: calculateDockerAbsoluteCpu(&v.PreCPUStats, &v.CPUStats),
|
|
||||||
Network: struct {
|
|
||||||
RxBytes uint64 `json:"rx_bytes"`
|
|
||||||
TxBytes uint64 `json:"tx_bytes"`
|
|
||||||
}{
|
|
||||||
RxBytes: rx,
|
|
||||||
TxBytes: tx,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if b, err := json.Marshal(st); err != nil {
|
|
||||||
l.WithField("error", errors.WithStack(err)).Warn("error while marshaling stats object for environment")
|
|
||||||
} else {
|
|
||||||
e.Events().Publish(environment.ResourceEvent, string(b))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The "docker stats" CLI call does not return the same value as the types.MemoryStats.Usage
|
|
||||||
// value which can be rather confusing to people trying to compare panel usage to
|
|
||||||
// their stats output.
|
|
||||||
//
|
|
||||||
// This math is straight up lifted from their CLI repository in order to show the same
|
|
||||||
// values to avoid people bothering me about it. It should also reflect a slightly more
|
|
||||||
// correct memory value anyways.
|
|
||||||
//
|
|
||||||
// @see https://github.com/docker/cli/blob/96e1d1d6/cli/command/container/stats_helpers.go#L227-L249
|
|
||||||
func calculateDockerMemory(stats types.MemoryStats) uint64 {
|
|
||||||
if v, ok := stats.Stats["total_inactive_file"]; ok && v < stats.Usage {
|
|
||||||
return stats.Usage - v
|
|
||||||
}
|
|
||||||
|
|
||||||
if v := stats.Stats["inactive_file"]; v < stats.Usage {
|
|
||||||
return stats.Usage - v
|
|
||||||
}
|
|
||||||
|
|
||||||
return stats.Usage
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculates the absolute CPU usage used by the server process on the system, not constrained
|
|
||||||
// by the defined CPU limits on the container.
|
|
||||||
//
|
|
||||||
// @see https://github.com/docker/cli/blob/aa097cf1aa19099da70930460250797c8920b709/cli/command/container/stats_helpers.go#L166
|
|
||||||
func calculateDockerAbsoluteCpu(pStats *types.CPUStats, stats *types.CPUStats) float64 {
|
|
||||||
// Calculate the change in CPU usage between the current and previous reading.
|
|
||||||
cpuDelta := float64(stats.CPUUsage.TotalUsage) - float64(pStats.CPUUsage.TotalUsage)
|
|
||||||
|
|
||||||
// Calculate the change for the entire system's CPU usage between current and previous reading.
|
|
||||||
systemDelta := float64(stats.SystemUsage) - float64(pStats.SystemUsage)
|
|
||||||
|
|
||||||
// Calculate the total number of CPU cores being used.
|
|
||||||
cpus := float64(stats.OnlineCPUs)
|
|
||||||
if cpus == 0.0 {
|
|
||||||
cpus = float64(len(stats.CPUUsage.PercpuUsage))
|
|
||||||
}
|
|
||||||
|
|
||||||
percent := 0.0
|
|
||||||
if systemDelta > 0.0 && cpuDelta > 0.0 {
|
|
||||||
percent = (cpuDelta / systemDelta) * cpus * 100.0
|
|
||||||
}
|
|
||||||
|
|
||||||
return math.Round(percent*1000) / 1000
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
package docker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/pterodactyl/wings/environment"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
type dockerLogLine struct {
|
|
||||||
Log string `json:"log"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Environment) setStream(s *types.HijackedResponse) {
|
|
||||||
e.mu.Lock()
|
|
||||||
e.stream = s
|
|
||||||
e.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sends the specified command to the stdin of the running container instance. There is no
|
|
||||||
// confirmation that this data is sent successfully, only that it gets pushed into the stdin.
|
|
||||||
func (e *Environment) SendCommand(c string) error {
|
|
||||||
e.mu.RLock()
|
|
||||||
defer e.mu.RUnlock()
|
|
||||||
|
|
||||||
if !e.IsAttached() {
|
|
||||||
return errors.New("attempting to send command to non-attached instance")
|
|
||||||
}
|
|
||||||
|
|
||||||
if e.meta.Stop != nil {
|
|
||||||
// If the command being processed is the same as the process stop command then we want to mark
|
|
||||||
// the server as entering the stopping state otherwise the process will stop and Wings will think
|
|
||||||
// it has crashed and attempt to restart it.
|
|
||||||
if e.meta.Stop.Type == "command" && c == e.meta.Stop.Value {
|
|
||||||
e.Events().Publish(environment.StateChangeEvent, environment.ProcessStoppingState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := e.stream.Conn.Write([]byte(c + "\n"))
|
|
||||||
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reads the log file for the server. This does not care if the server is running or not, it will
|
|
||||||
// simply try to read the last X bytes of the file and return them.
|
|
||||||
func (e *Environment) Readlog(lines int) ([]string, error) {
|
|
||||||
r, err := e.client.ContainerLogs(context.Background(), e.Id, types.ContainerLogsOptions{
|
|
||||||
ShowStdout: true,
|
|
||||||
ShowStderr: true,
|
|
||||||
Tail: strconv.Itoa(lines),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
defer r.Close()
|
|
||||||
|
|
||||||
var out []string
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(r)
|
|
||||||
for scanner.Scan() {
|
|
||||||
out = append(out, scanner.Text())
|
|
||||||
}
|
|
||||||
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Docker stores the logs for server output in a JSON format. This function will iterate over the JSON
|
|
||||||
// that was read from the log file and parse it into a more human readable format.
|
|
||||||
func (e *Environment) parseLogToStrings(b []byte) ([]string, error) {
|
|
||||||
var hasError = false
|
|
||||||
var out []string
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(bytes.NewReader(b))
|
|
||||||
for scanner.Scan() {
|
|
||||||
var l dockerLogLine
|
|
||||||
|
|
||||||
// Unmarshal the contents and allow up to a single error before bailing out of the process. We
|
|
||||||
// do this because if you're arbitrarily reading a length of the file you'll likely end up
|
|
||||||
// with the first line in the output being improperly formatted JSON. In those cases we want to
|
|
||||||
// just skip over it. However if we see another error we're going to bail out because that is an
|
|
||||||
// abnormal situation.
|
|
||||||
if err := json.Unmarshal([]byte(scanner.Text()), &l); err != nil {
|
|
||||||
if hasError {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
hasError = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
out = append(out, l.Log)
|
|
||||||
}
|
|
||||||
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
package environment
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/apex/log"
|
|
||||||
"math"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Mount struct {
|
|
||||||
// In Docker environments this makes no difference, however in a non-Docker environment you
|
|
||||||
// should treat the "Default" mount as the root directory for the server. All other mounts
|
|
||||||
// are just in addition to that one, and generally things like shared maps or timezone data.
|
|
||||||
Default bool `json:"-"`
|
|
||||||
|
|
||||||
// The target path on the system. This is "/home/container" for all server's Default mount
|
|
||||||
// but in non-container environments you can likely ignore the target and just work with the
|
|
||||||
// source.
|
|
||||||
Target string `json:"target"`
|
|
||||||
|
|
||||||
// The directory from which the files will be read. In Docker environments this is the directory
|
|
||||||
// that we're mounting into the container at the Target location.
|
|
||||||
Source string `json:"source"`
|
|
||||||
|
|
||||||
// Whether or not the directory is being mounted as read-only. It is up to the environment to
|
|
||||||
// handle this value correctly and ensure security expectations are met with its usage.
|
|
||||||
ReadOnly bool `json:"read_only"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// The build settings for a given server that impact docker container creation and
|
|
||||||
// resource limits for a server instance.
|
|
||||||
type Limits struct {
|
|
||||||
// The total amount of memory in megabytes that this server is allowed to
|
|
||||||
// use on the host system.
|
|
||||||
MemoryLimit int64 `json:"memory_limit"`
|
|
||||||
|
|
||||||
// The amount of additional swap space to be provided to a container instance.
|
|
||||||
Swap int64 `json:"swap"`
|
|
||||||
|
|
||||||
// The relative weight for IO operations in a container. This is relative to other
|
|
||||||
// containers on the system and should be a value between 10 and 1000.
|
|
||||||
IoWeight uint16 `json:"io_weight"`
|
|
||||||
|
|
||||||
// The percentage of CPU that this instance is allowed to consume relative to
|
|
||||||
// the host. A value of 200% represents complete utilization of two cores. This
|
|
||||||
// should be a value between 1 and THREAD_COUNT * 100.
|
|
||||||
CpuLimit int64 `json:"cpu_limit"`
|
|
||||||
|
|
||||||
// The amount of disk space in megabytes that a server is allowed to use.
|
|
||||||
DiskSpace int64 `json:"disk_space"`
|
|
||||||
|
|
||||||
// Sets which CPU threads can be used by the docker instance.
|
|
||||||
Threads string `json:"threads"`
|
|
||||||
|
|
||||||
OOMDisabled bool `json:"oom_disabled"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Converts the CPU limit for a server build into a number that can be better understood
|
|
||||||
// by the Docker environment. If there is no limit set, return -1 which will indicate to
|
|
||||||
// Docker that it has unlimited CPU quota.
|
|
||||||
func (r *Limits) ConvertedCpuLimit() int64 {
|
|
||||||
if r.CpuLimit == 0 {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.CpuLimit * 1000
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the hard limit for memory usage to be 5% more than the amount of memory assigned to
|
|
||||||
// the server. If the memory limit for the server is < 4G, use 10%, if less than 2G use
|
|
||||||
// 15%. This avoids unexpected crashes from processes like Java which run over the limit.
|
|
||||||
func (r *Limits) MemoryOverheadMultiplier() float64 {
|
|
||||||
if r.MemoryLimit <= 2048 {
|
|
||||||
return 1.15
|
|
||||||
} else if r.MemoryLimit <= 4096 {
|
|
||||||
return 1.10
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1.05
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Limits) BoundedMemoryLimit() int64 {
|
|
||||||
return int64(math.Round(float64(r.MemoryLimit) * r.MemoryOverheadMultiplier() * 1_000_000))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the amount of swap available as a total in bytes. This is returned as the amount
|
|
||||||
// of memory available to the server initially, PLUS the amount of additional swap to include
|
|
||||||
// which is the format used by Docker.
|
|
||||||
func (r *Limits) ConvertedSwap() int64 {
|
|
||||||
if r.Swap < 0 {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
return (r.Swap * 1_000_000) + r.BoundedMemoryLimit()
|
|
||||||
}
|
|
||||||
|
|
||||||
type Variables map[string]interface{}
|
|
||||||
|
|
||||||
// Ugly hacky function to handle environment variables that get passed through as not-a-string
|
|
||||||
// from the Panel. Ideally we'd just say only pass strings, but that is a fragile idea and if a
|
|
||||||
// string wasn't passed through you'd cause a crash or the server to become unavailable. For now
|
|
||||||
// try to handle the most likely values from the JSON and hope for the best.
|
|
||||||
func (v Variables) Get(key string) string {
|
|
||||||
val, ok := v[key]
|
|
||||||
if !ok {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
switch val.(type) {
|
|
||||||
case int:
|
|
||||||
return strconv.Itoa(val.(int))
|
|
||||||
case int32:
|
|
||||||
return strconv.FormatInt(val.(int64), 10)
|
|
||||||
case int64:
|
|
||||||
return strconv.FormatInt(val.(int64), 10)
|
|
||||||
case float32:
|
|
||||||
return fmt.Sprintf("%f", val.(float32))
|
|
||||||
case float64:
|
|
||||||
return fmt.Sprintf("%f", val.(float64))
|
|
||||||
case bool:
|
|
||||||
return strconv.FormatBool(val.(bool))
|
|
||||||
case string:
|
|
||||||
return val.(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: I think we can add a check for val == nil and return an empty string for those
|
|
||||||
// and this warning should theoretically never happen?
|
|
||||||
log.Warn(fmt.Sprintf("failed to marshal environment variable \"%s\" of type %+v into string", key, val))
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package environment
|
|
||||||
|
|
||||||
import "sync"
|
|
||||||
|
|
||||||
// Defines the current resource usage for a given server instance. If a server is offline you
|
|
||||||
// should obviously expect memory and CPU usage to be 0. However, disk will always be returned
|
|
||||||
// since that is not dependent on the server being running to collect that data.
|
|
||||||
type Stats struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
|
|
||||||
// The total amount of memory, in bytes, that this server instance is consuming. This is
|
|
||||||
// calculated slightly differently than just using the raw Memory field that the stats
|
|
||||||
// return from the container, so please check the code setting this value for how that
|
|
||||||
// is calculated.
|
|
||||||
Memory uint64 `json:"memory_bytes"`
|
|
||||||
|
|
||||||
// The total amount of memory this container or resource can use. Inside Docker this is
|
|
||||||
// going to be higher than you'd expect because we're automatically allocating overhead
|
|
||||||
// abilities for the container, so its not going to be a perfect match.
|
|
||||||
MemoryLimit uint64 `json:"memory_limit_bytes"`
|
|
||||||
|
|
||||||
// The absolute CPU usage is the amount of CPU used in relation to the entire system and
|
|
||||||
// does not take into account any limits on the server process itself.
|
|
||||||
CpuAbsolute float64 `json:"cpu_absolute"`
|
|
||||||
|
|
||||||
// The current disk space being used by the server. This is cached to prevent slow lookup
|
|
||||||
// issues on frequent refreshes.
|
|
||||||
// Disk int64 `json:"disk_bytes"`
|
|
||||||
|
|
||||||
// Current network transmit in & out for a container.
|
|
||||||
Network struct {
|
|
||||||
RxBytes uint64 `json:"rx_bytes"`
|
|
||||||
TxBytes uint64 `json:"tx_bytes"`
|
|
||||||
} `json:"network"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resets the usages values to zero, used when a server is stopped to ensure we don't hold
|
|
||||||
// onto any values incorrectly.
|
|
||||||
func (s *Stats) Empty() {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
|
|
||||||
s.Memory = 0
|
|
||||||
s.CpuAbsolute = 0
|
|
||||||
s.Network.TxBytes = 0
|
|
||||||
s.Network.RxBytes = 0
|
|
||||||
}
|
|
||||||
105
events/events.go
105
events/events.go
@@ -1,105 +0,0 @@
|
|||||||
package events
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Event struct {
|
|
||||||
Data string
|
|
||||||
Topic string
|
|
||||||
}
|
|
||||||
|
|
||||||
type EventBus struct {
|
|
||||||
sync.RWMutex
|
|
||||||
|
|
||||||
subscribers map[string]map[chan Event]struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func New() *EventBus {
|
|
||||||
return &EventBus{
|
|
||||||
subscribers: make(map[string]map[chan Event]struct{}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Publish data to a given topic.
|
|
||||||
func (e *EventBus) Publish(topic string, data string) {
|
|
||||||
t := topic
|
|
||||||
// Some of our topics for the socket support passing a more specific namespace,
|
|
||||||
// such as "backup completed:1234" to indicate which specific backup was completed.
|
|
||||||
//
|
|
||||||
// In these cases, we still need to the send the event using the standard listener
|
|
||||||
// name of "backup completed".
|
|
||||||
if strings.Contains(topic, ":") {
|
|
||||||
parts := strings.SplitN(topic, ":", 2)
|
|
||||||
|
|
||||||
if len(parts) == 2 {
|
|
||||||
t = parts[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Acquire a read lock and loop over all of the channels registered for the topic. This
|
|
||||||
// avoids a panic crash if the process tries to unregister the channel while this routine
|
|
||||||
// is running.
|
|
||||||
go func() {
|
|
||||||
e.RLock()
|
|
||||||
defer e.RUnlock()
|
|
||||||
|
|
||||||
if ch, ok := e.subscribers[t]; ok {
|
|
||||||
for channel := range ch {
|
|
||||||
channel <- Event{Data: data, Topic: topic}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *EventBus) PublishJson(topic string, data interface{}) error {
|
|
||||||
b, err := json.Marshal(data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
e.Publish(topic, string(b))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscribe to an emitter topic using a channel.
|
|
||||||
func (e *EventBus) Subscribe(topic string, ch chan Event) {
|
|
||||||
e.Lock()
|
|
||||||
defer e.Unlock()
|
|
||||||
|
|
||||||
if _, exists := e.subscribers[topic]; !exists {
|
|
||||||
e.subscribers[topic] = make(map[chan Event]struct{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only set the channel if there is not currently a matching one for this topic. This
|
|
||||||
// avoids registering two identical listeners for the same topic and causing pain in
|
|
||||||
// the unsubscribe functionality as well.
|
|
||||||
if _, exists := e.subscribers[topic][ch]; !exists {
|
|
||||||
e.subscribers[topic][ch] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unsubscribe a channel from a given topic.
|
|
||||||
func (e *EventBus) Unsubscribe(topic string, ch chan Event) {
|
|
||||||
e.Lock()
|
|
||||||
defer e.Unlock()
|
|
||||||
|
|
||||||
if _, exists := e.subscribers[topic][ch]; exists {
|
|
||||||
delete(e.subscribers[topic], ch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Removes all of the event listeners for the server. This is used when a server
|
|
||||||
// is being deleted to avoid a bunch of de-reference errors cropping up. Obviously
|
|
||||||
// should also check elsewhere and handle a server reference going nil, but this
|
|
||||||
// won't hurt.
|
|
||||||
func (e *EventBus) UnsubscribeAll() {
|
|
||||||
e.Lock()
|
|
||||||
defer e.Unlock()
|
|
||||||
|
|
||||||
// Reset the entire struct into an empty map.
|
|
||||||
e.subscribers = make(map[string]map[chan Event]struct{})
|
|
||||||
}
|
|
||||||
91
go.mod
91
go.mod
@@ -2,80 +2,75 @@ module github.com/pterodactyl/wings
|
|||||||
|
|
||||||
go 1.13
|
go 1.13
|
||||||
|
|
||||||
|
// Uncomment this in development environments to make changes to the core SFTP
|
||||||
|
// server software. This assumes you're using the official Pterodactyl Environment
|
||||||
|
// otherwise this path will not work.
|
||||||
|
//
|
||||||
|
// @see https://github.com/pterodactyl/development
|
||||||
|
//
|
||||||
|
// replace github.com/pterodactyl/sftp-server => ../sftp-server
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/AlecAivazis/survey/v2 v2.1.0
|
github.com/AlecAivazis/survey/v2 v2.0.7
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
|
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
|
||||||
github.com/Jeffail/gabs/v2 v2.5.1
|
github.com/Jeffail/gabs/v2 v2.2.0
|
||||||
github.com/Microsoft/go-winio v0.4.14 // indirect
|
github.com/Microsoft/go-winio v0.4.7 // indirect
|
||||||
github.com/NYTimes/logrotate v1.0.0
|
github.com/apex/log v1.3.0
|
||||||
github.com/andybalholm/brotli v1.0.0 // indirect
|
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a
|
||||||
github.com/apex/log v1.8.0
|
|
||||||
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535
|
|
||||||
github.com/beevik/etree v1.1.0
|
github.com/beevik/etree v1.1.0
|
||||||
github.com/buger/jsonparser v1.0.0
|
github.com/buger/jsonparser v0.0.0-20191204142016-1a29609e0929
|
||||||
github.com/cobaugh/osrelease v0.0.0-20181218015638-a93a0a55a249
|
github.com/cobaugh/osrelease v0.0.0-20181218015638-a93a0a55a249
|
||||||
github.com/containerd/containerd v1.3.7 // indirect
|
github.com/containerd/containerd v1.3.6 // indirect
|
||||||
github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b // indirect
|
github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448 // indirect
|
||||||
github.com/creasty/defaults v1.5.0
|
github.com/creasty/defaults v1.3.0
|
||||||
github.com/docker/cli v17.12.1-ce-rc2+incompatible
|
github.com/docker/cli v17.12.1-ce-rc2+incompatible
|
||||||
github.com/docker/distribution v2.7.1+incompatible // indirect
|
github.com/docker/distribution v2.7.1+incompatible // indirect
|
||||||
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible
|
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible
|
||||||
github.com/docker/go-connections v0.4.0
|
github.com/docker/go-connections v0.4.0
|
||||||
github.com/docker/go-metrics v0.0.1 // indirect
|
github.com/docker/go-metrics v0.0.1 // indirect
|
||||||
github.com/docker/go-units v0.4.0 // indirect
|
github.com/docker/go-units v0.3.3 // indirect
|
||||||
github.com/fatih/color v1.9.0
|
github.com/fatih/color v1.9.0
|
||||||
github.com/frankban/quicktest v1.10.2 // indirect
|
github.com/gabriel-vasile/mimetype v0.1.4
|
||||||
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
github.com/gammazero/workerpool v0.0.0-20200608033439-1a5ca90a5753
|
||||||
github.com/gabriel-vasile/mimetype v1.1.1
|
github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.0
|
||||||
github.com/gammazero/deque v0.0.0-20200721202602-07291166fe33 // indirect
|
|
||||||
github.com/gammazero/workerpool v1.0.0
|
|
||||||
github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.2
|
|
||||||
github.com/gin-gonic/gin v1.6.3
|
github.com/gin-gonic/gin v1.6.3
|
||||||
github.com/go-playground/validator/v10 v10.3.0 // indirect
|
github.com/golang/protobuf v1.3.5 // indirect
|
||||||
github.com/gogo/protobuf v1.3.1 // indirect
|
|
||||||
github.com/google/uuid v1.1.1
|
github.com/google/uuid v1.1.1
|
||||||
github.com/gorilla/mux v1.7.4 // indirect
|
github.com/gorilla/mux v1.7.4 // indirect
|
||||||
github.com/gorilla/websocket v1.4.2
|
github.com/gorilla/websocket v1.4.0
|
||||||
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334
|
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334
|
||||||
github.com/icza/dyno v0.0.0-20200205103839-49cb13720835
|
github.com/icza/dyno v0.0.0-20200205103839-49cb13720835
|
||||||
github.com/imdario/mergo v0.3.8
|
github.com/imdario/mergo v0.3.8
|
||||||
github.com/karrick/godirwalk v1.16.1
|
github.com/klauspost/pgzip v1.2.3
|
||||||
github.com/klauspost/compress v1.10.10 // indirect
|
|
||||||
github.com/klauspost/pgzip v1.2.4
|
|
||||||
github.com/magefile/mage v1.10.0 // indirect
|
|
||||||
github.com/magiconair/properties v1.8.1
|
github.com/magiconair/properties v1.8.1
|
||||||
github.com/mattn/go-colorable v0.1.7
|
github.com/mattn/go-colorable v0.1.4
|
||||||
github.com/mattn/go-shellwords v1.0.10 // indirect
|
github.com/mattn/go-shellwords v1.0.10 // indirect
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
|
||||||
github.com/mholt/archiver/v3 v3.3.0
|
github.com/mholt/archiver/v3 v3.3.0
|
||||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db
|
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db
|
||||||
github.com/morikuni/aec v1.0.0 // indirect
|
github.com/morikuni/aec v1.0.0 // indirect
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||||
github.com/nwaples/rardecode v1.1.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
|
||||||
github.com/opencontainers/image-spec v1.0.1 // indirect
|
github.com/opencontainers/image-spec v1.0.1 // indirect
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
github.com/pierrec/lz4 v2.5.2+incompatible // indirect
|
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/pkg/profile v1.5.0
|
github.com/pkg/profile v1.4.0
|
||||||
github.com/pkg/sftp v1.11.0
|
github.com/pkg/sftp v1.11.0 // indirect
|
||||||
github.com/prometheus/common v0.11.1 // indirect
|
github.com/pterodactyl/sftp-server v1.1.4
|
||||||
github.com/remeh/sizedwaitgroup v1.0.0
|
github.com/remeh/sizedwaitgroup v0.0.0-20180822144253-5e7302b12cce
|
||||||
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94
|
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94
|
||||||
github.com/spf13/cobra v1.0.0
|
github.com/smartystreets/goconvey v1.6.4 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/cobra v0.0.7
|
||||||
github.com/ulikunitz/xz v0.5.7 // indirect
|
github.com/stretchr/testify v1.5.1 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de
|
go.uber.org/zap v1.15.0
|
||||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381 // indirect
|
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79
|
||||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect
|
||||||
golang.org/x/text v0.3.3 // indirect
|
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 // indirect
|
||||||
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect
|
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f // indirect
|
||||||
google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98 // indirect
|
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5 // indirect
|
||||||
google.golang.org/grpc v1.31.0 // indirect
|
|
||||||
google.golang.org/protobuf v1.25.0 // indirect
|
|
||||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||||
gopkg.in/ini.v1 v1.57.0
|
gopkg.in/ini.v1 v1.51.0
|
||||||
gopkg.in/yaml.v2 v2.3.0
|
gopkg.in/yaml.v2 v2.2.8
|
||||||
gotest.tools v2.2.0+incompatible // indirect
|
gotest.tools v2.2.0+incompatible // indirect
|
||||||
|
honnef.co/go/tools v0.0.1-2020.1.3 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
456
go.sum
456
go.sum
@@ -1,52 +1,30 @@
|
|||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
github.com/AlecAivazis/survey/v2 v2.0.7 h1:+f825XHLse/hWd2tE/V5df04WFGimk34Eyg/z35w/rc=
|
||||||
github.com/AlecAivazis/survey/v2 v2.1.0 h1:AT4+23hOFopXYZaNGugbk7MWItkz0SfTmH/Hk92KeeE=
|
github.com/AlecAivazis/survey/v2 v2.0.7/go.mod h1:mlizQTaPjnR4jcpwRSaSlkbsRfYFEyKgLQvYTzxxiHA=
|
||||||
github.com/AlecAivazis/survey/v2 v2.1.0/go.mod h1:9FJRdMdDm8rnT+zHVbvQT2RTSTLq0Ttd6q3Vl2fahjk=
|
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
|
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/Jeffail/gabs/v2 v2.5.1 h1:ANfZYjpMlfTTKebycu4X1AgkVWumFVDYQl7JwOr4mDk=
|
github.com/Jeffail/gabs/v2 v2.2.0 h1:7touC+WzbQ7LO5+mwgxT44miyTqAVCOlIWLA6PiIB5w=
|
||||||
github.com/Jeffail/gabs/v2 v2.5.1/go.mod h1:xCn81vdHKxFUuWWAaD5jCTQDNPBMh5pPs9IJ+NcziBI=
|
github.com/Jeffail/gabs/v2 v2.2.0/go.mod h1:xCn81vdHKxFUuWWAaD5jCTQDNPBMh5pPs9IJ+NcziBI=
|
||||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
|
github.com/Microsoft/go-winio v0.4.7 h1:vOvDiY/F1avSWlCWiKJjdYKz2jVjTK3pWPHndeG4OAY=
|
||||||
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
|
github.com/Microsoft/go-winio v0.4.7/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
||||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
|
||||||
github.com/NYTimes/logrotate v1.0.0 h1:6jFGbon6jOtpy3t3kwZZKS4Gdmf1C/Wv5J4ll4Xn5yk=
|
|
||||||
github.com/NYTimes/logrotate v1.0.0/go.mod h1:GxNz1cSw1c6t99PXoZlw+nm90H6cyQyrH66pjVv7x88=
|
|
||||||
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
|
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
|
||||||
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
|
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
|
||||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
|
||||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
|
||||||
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
|
|
||||||
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
|
|
||||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
|
||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
|
||||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
|
||||||
github.com/andybalholm/brotli v0.0.0-20190621154722-5f990b63d2d6 h1:bZ28Hqta7TFAK3Q08CMvv8y3/8ATaEqv2nGoc6yff6c=
|
github.com/andybalholm/brotli v0.0.0-20190621154722-5f990b63d2d6 h1:bZ28Hqta7TFAK3Q08CMvv8y3/8ATaEqv2nGoc6yff6c=
|
||||||
github.com/andybalholm/brotli v0.0.0-20190621154722-5f990b63d2d6/go.mod h1:+lx6/Aqd1kLJ1GQfkvOnaZ1WGmLpMpbprPuIOOZX30U=
|
github.com/andybalholm/brotli v0.0.0-20190621154722-5f990b63d2d6/go.mod h1:+lx6/Aqd1kLJ1GQfkvOnaZ1WGmLpMpbprPuIOOZX30U=
|
||||||
github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4=
|
github.com/apex/log v1.3.0 h1:1fyfbPvUwD10nMoh3hY6MXzvZShJQn9/ck7ATgAt5pA=
|
||||||
github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
github.com/apex/log v1.3.0/go.mod h1:jd8Vpsr46WAe3EZSQ/IUMs2qQD/GOycT5rPWCO1yGcs=
|
||||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
github.com/apex/logs v0.0.4/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo=
|
||||||
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
|
||||||
github.com/apex/log v1.8.0 h1:+W4j+dttibFvynPLlctdnYFUn1eLKT37BZWWW2iMfEM=
|
|
||||||
github.com/apex/log v1.8.0/go.mod h1:m82fZlWIuiWzWP04XCTXmnX0xRkYYbCdYn8jbJeLBEA=
|
|
||||||
github.com/apex/logs v1.0.0/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo=
|
|
||||||
github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE=
|
github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE=
|
||||||
github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys=
|
github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys=
|
||||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
|
||||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
|
||||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||||
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
|
||||||
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY=
|
|
||||||
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
|
||||||
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
|
|
||||||
github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||||
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
|
||||||
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
|
|
||||||
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 h1:WWB576BN5zNSZc/M9d/10pqEx5VHNhaQ/yOVAkmj5Yo=
|
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 h1:WWB576BN5zNSZc/M9d/10pqEx5VHNhaQ/yOVAkmj5Yo=
|
||||||
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
|
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
|
||||||
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
|
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
|
||||||
@@ -55,39 +33,24 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
|
|||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
github.com/buger/jsonparser v0.0.0-20191204142016-1a29609e0929 h1:MW/JDk68Rny52yI0M0N+P8lySNgB+NhpI/uAmhgOhUM=
|
||||||
github.com/buger/jsonparser v1.0.0 h1:etJTGF5ESxjI0Ic2UaLQs2LQQpa8G9ykQScukbh4L8A=
|
github.com/buger/jsonparser v0.0.0-20191204142016-1a29609e0929/go.mod h1:tgcrVJ81GPSF0mz+0nu1Xaz0fazGPrmmJfJtxjbHhUQ=
|
||||||
github.com/buger/jsonparser v1.0.0/go.mod h1:tgcrVJ81GPSF0mz+0nu1Xaz0fazGPrmmJfJtxjbHhUQ=
|
|
||||||
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
|
|
||||||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
|
||||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
|
||||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
|
||||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
|
||||||
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
|
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
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/cobaugh/osrelease v0.0.0-20181218015638-a93a0a55a249 h1:R0IDH8daQ3lODvu8YtxnIqqth5qMGCJyADoUQvmLx4o=
|
github.com/cobaugh/osrelease v0.0.0-20181218015638-a93a0a55a249 h1:R0IDH8daQ3lODvu8YtxnIqqth5qMGCJyADoUQvmLx4o=
|
||||||
github.com/cobaugh/osrelease v0.0.0-20181218015638-a93a0a55a249/go.mod h1:EHKW9yNEYSBpTKzuu7Y9oOrft/UlzH57rMIB03oev6M=
|
github.com/cobaugh/osrelease v0.0.0-20181218015638-a93a0a55a249/go.mod h1:EHKW9yNEYSBpTKzuu7Y9oOrft/UlzH57rMIB03oev6M=
|
||||||
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
|
github.com/containerd/containerd v1.3.6 h1:SMfcKoQyWhaRsYq7290ioC6XFcHDNcHvcEMjF6ORpac=
|
||||||
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
|
github.com/containerd/containerd v1.3.6/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
||||||
github.com/containerd/containerd v1.3.7 h1:eFSOChY8TTcxvkzp8g+Ov1RL0MYww7XEeK0y+zqGpVc=
|
github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448 h1:PUD50EuOMkXVcpBIA/R95d56duJR9VxhwncsFbNnxW4=
|
||||||
github.com/containerd/containerd v1.3.7/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
|
||||||
github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b h1:qUtCegLdOUVfVJOw+KDg6eJyE1TGvLlkGEd1091kSSQ=
|
|
||||||
github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0=
|
|
||||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
|
||||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||||
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
|
||||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
github.com/creasty/defaults v1.3.0 h1:uG+RAxYbJgOPCOdKEcec9ZJXeva7Y6mj/8egdzwmLtw=
|
||||||
github.com/creasty/defaults v1.5.0 h1:DW6NAGGaKuNSKkntc8BCBrR2KOUAcXVnfcwu/LmJhaQ=
|
github.com/creasty/defaults v1.3.0/go.mod h1:CIEEvs7oIVZm30R8VxtFJs+4k201gReYyuYHJxZc68I=
|
||||||
github.com/creasty/defaults v1.5.0/go.mod h1:FPZ+Y0WNrbqOVw+c6av63eyHUAl6pMHZwqLPvXUZGfY=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 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.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -103,43 +66,25 @@ github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKoh
|
|||||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||||
github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
|
github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
|
||||||
github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
|
github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
|
||||||
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk=
|
||||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
|
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
|
||||||
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
|
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
|
||||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
|
||||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
|
||||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
|
||||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
|
||||||
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
|
||||||
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
|
|
||||||
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/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
|
||||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||||
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
|
|
||||||
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
|
|
||||||
github.com/frankban/quicktest v1.10.2 h1:19ARM85nVi4xH7xPXuc5eM/udya5ieh7b/Sv+d844Tk=
|
|
||||||
github.com/frankban/quicktest v1.10.2/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P6txr3mVT54s=
|
|
||||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
github.com/gabriel-vasile/mimetype v0.1.4 h1:5mcsq3+DXypREUkW+1juhjeKmE/XnWgs+paHMJn7lf8=
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
github.com/gabriel-vasile/mimetype v0.1.4/go.mod h1:kMJbg3SlWZCsj4R73F1WDzbT9AyGCOVmUtIxxwO5pmI=
|
||||||
github.com/gabriel-vasile/mimetype v1.1.1 h1:qbN9MPuRf3bstHu9zkI9jDWNfH//9+9kHxr9oRBBBOA=
|
|
||||||
github.com/gabriel-vasile/mimetype v1.1.1/go.mod h1:6CDPel/o/3/s4+bp6kIbsWATq8pmgOisOPG40CJa6To=
|
|
||||||
github.com/gammazero/deque v0.0.0-20200227231300-1e9af0e52b46 h1:iX4+rD9Fjdx8SkmSO/O5WAIX/j79ll3kuqv5VdYt9J8=
|
github.com/gammazero/deque v0.0.0-20200227231300-1e9af0e52b46 h1:iX4+rD9Fjdx8SkmSO/O5WAIX/j79ll3kuqv5VdYt9J8=
|
||||||
github.com/gammazero/deque v0.0.0-20200227231300-1e9af0e52b46/go.mod h1:D90+MBHVc9Sk1lJAbEVgws0eYEurY4mv2TDso3Nxh3w=
|
github.com/gammazero/deque v0.0.0-20200227231300-1e9af0e52b46/go.mod h1:D90+MBHVc9Sk1lJAbEVgws0eYEurY4mv2TDso3Nxh3w=
|
||||||
github.com/gammazero/deque v0.0.0-20200721202602-07291166fe33 h1:UG4wNrJX9xSKnm/Gck5yTbxnOhpNleuE4MQRdmcGySo=
|
github.com/gammazero/workerpool v0.0.0-20200608033439-1a5ca90a5753 h1:oSQ61LxZkz3Z4La0O5cbyVDvLWEfbNgiD43cSPdjPQQ=
|
||||||
github.com/gammazero/deque v0.0.0-20200721202602-07291166fe33/go.mod h1:D90+MBHVc9Sk1lJAbEVgws0eYEurY4mv2TDso3Nxh3w=
|
github.com/gammazero/workerpool v0.0.0-20200608033439-1a5ca90a5753/go.mod h1:/XWO2YAUUpPi3smDlFBl0vpX0JHwUomDM/oRMwRmnSs=
|
||||||
github.com/gammazero/workerpool v1.0.0 h1:MfkJc6KL0tAmjrRDS203AZz3F+84Uod9YbL8KjpcQ00=
|
github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.0 h1:7KeiSrO5puFH1+vdAdbpiie2TrNnkvFc/eOQzT60Z2k=
|
||||||
github.com/gammazero/workerpool v1.0.0/go.mod h1:/XWO2YAUUpPi3smDlFBl0vpX0JHwUomDM/oRMwRmnSs=
|
github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.0/go.mod h1:D1+3UtCYAJ1os1PI+zhTVEj6Tb+IHJvXjXKz83OstmM=
|
||||||
github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.2 h1:3t7jvTkeQfk1FdP0noXSNiM6AdBokLz7QmZDmnCHAAA=
|
|
||||||
github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.2/go.mod h1:AncDcjXz18xetI3A6STfXq2w+LuTx8pQ8bGEwRN8zVM=
|
|
||||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
@@ -147,12 +92,9 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm
|
|||||||
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
|
||||||
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
|
|
||||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
|
github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
|
||||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
|
||||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||||
@@ -161,98 +103,46 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87
|
|||||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||||
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
|
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
|
||||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||||
github.com/go-playground/validator/v10 v10.3.0 h1:nZU+7q+yJoFmwvNgv/LnPUkwPal62+b2xXj0AU1Es7o=
|
|
||||||
github.com/go-playground/validator/v10 v10.3.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
|
||||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
|
|
||||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
|
||||||
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
|
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
|
||||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||||
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
|
|
||||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
|
||||||
github.com/golang/gddo v0.0.0-20190419222130-af0f2af80721 h1:KRMr9A3qfbVM7iV/WcLY/rL5LICqwMHLhwRXKu99fXw=
|
github.com/golang/gddo v0.0.0-20190419222130-af0f2af80721 h1:KRMr9A3qfbVM7iV/WcLY/rL5LICqwMHLhwRXKu99fXw=
|
||||||
github.com/golang/gddo v0.0.0-20190419222130-af0f2af80721/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4=
|
github.com/golang/gddo v0.0.0-20190419222130-af0f2af80721/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
|
||||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
|
||||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
|
||||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
|
||||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
|
||||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
|
||||||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
|
||||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
|
||||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
|
||||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
|
||||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
|
|
||||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
|
|
||||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
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 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
|
||||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
|
||||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
|
||||||
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
|
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
|
||||||
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
|
||||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
|
||||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
|
||||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||||
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
|
||||||
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
|
|
||||||
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
|
||||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
|
||||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
|
||||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
|
||||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
|
||||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
|
||||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
|
||||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
|
||||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
|
||||||
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
|
||||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
|
||||||
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/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
|
||||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
|
||||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
|
||||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
|
||||||
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
|
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
|
||||||
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
|
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
|
|
||||||
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334 h1:VHgatEHNcBFEB7inlalqfNqw65aNkM1lGX2yt3NmbS8=
|
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334 h1:VHgatEHNcBFEB7inlalqfNqw65aNkM1lGX2yt3NmbS8=
|
||||||
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE=
|
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE=
|
||||||
github.com/icza/dyno v0.0.0-20200205103839-49cb13720835 h1:f1irK5f03uGGj+FjgQfZ5VhdKNVQVJ4skHsedzVohQ4=
|
github.com/icza/dyno v0.0.0-20200205103839-49cb13720835 h1:f1irK5f03uGGj+FjgQfZ5VhdKNVQVJ4skHsedzVohQ4=
|
||||||
@@ -261,52 +151,37 @@ github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
|
|||||||
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
|
|
||||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||||
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
|
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
|
||||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
|
||||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
|
||||||
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
|
|
||||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
|
||||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g=
|
github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g=
|
||||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
|
||||||
github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw=
|
|
||||||
github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=
|
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||||
github.com/klauspost/compress v1.9.2 h1:LfVyl+ZlLlLDeQ/d2AqfGIIH4qEDu0Ed2S5GyhCWIWY=
|
github.com/klauspost/compress v1.9.2 h1:LfVyl+ZlLlLDeQ/d2AqfGIIH4qEDu0Ed2S5GyhCWIWY=
|
||||||
github.com/klauspost/compress v1.9.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
github.com/klauspost/compress v1.9.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||||
github.com/klauspost/compress v1.10.10 h1:a/y8CglcM7gLGYmlbP/stPE5sR3hbhFRUjCBfd/0B3I=
|
|
||||||
github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
|
||||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||||
github.com/klauspost/pgzip v1.2.1 h1:oIPZROsWuPHpOdMVWLuJZXwgjhrW8r1yEX8UqMyeNHM=
|
github.com/klauspost/pgzip v1.2.1 h1:oIPZROsWuPHpOdMVWLuJZXwgjhrW8r1yEX8UqMyeNHM=
|
||||||
github.com/klauspost/pgzip v1.2.1/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
github.com/klauspost/pgzip v1.2.1/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||||
github.com/klauspost/pgzip v1.2.4 h1:TQ7CNpYKovDOmqzRHKxJh0BeaBI7UdQZYc6p7pMQh1A=
|
github.com/klauspost/pgzip v1.2.3 h1:Ce2to9wvs/cuJ2b86/CKQoTYr9VHfpanYosZ0UBJqdw=
|
||||||
github.com/klauspost/pgzip v1.2.4/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
github.com/klauspost/pgzip v1.2.3/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
|
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
|
||||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ=
|
github.com/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ=
|
||||||
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
@@ -314,52 +189,32 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
|||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||||
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
|
|
||||||
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
|
|
||||||
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
|
|
||||||
github.com/magefile/mage v1.9.0 h1:t3AU2wNwehMCW97vuqQLtw6puppWXHO+O2MHo5a50XE=
|
github.com/magefile/mage v1.9.0 h1:t3AU2wNwehMCW97vuqQLtw6puppWXHO+O2MHo5a50XE=
|
||||||
github.com/magefile/mage v1.9.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
github.com/magefile/mage v1.9.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||||
github.com/magefile/mage v1.10.0 h1:3HiXzCUY12kh9bIuyXShaVe529fJfyqoVM42o/uom2g=
|
|
||||||
github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
|
||||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
|
||||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||||
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
|
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
|
||||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
|
|
||||||
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
|
||||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
|
||||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
|
||||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
|
||||||
github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw=
|
github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw=
|
||||||
github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
|
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
|
||||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
|
||||||
github.com/mholt/archiver/v3 v3.3.0 h1:vWjhY8SQp5yzM9P6OJ/eZEkmi3UAbRrxCq48MxjAzig=
|
github.com/mholt/archiver/v3 v3.3.0 h1:vWjhY8SQp5yzM9P6OJ/eZEkmi3UAbRrxCq48MxjAzig=
|
||||||
github.com/mholt/archiver/v3 v3.3.0/go.mod h1:YnQtqsp+94Rwd0D/rk5cnLrxusUBUXg+08Ebtr1Mqao=
|
github.com/mholt/archiver/v3 v3.3.0/go.mod h1:YnQtqsp+94Rwd0D/rk5cnLrxusUBUXg+08Ebtr1Mqao=
|
||||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
|
||||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
|
||||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
|
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/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
|
||||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
|
||||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
|
||||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
|
||||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
|
||||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
@@ -370,124 +225,69 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
|
|||||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
|
||||||
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
|
|
||||||
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
|
|
||||||
github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
|
|
||||||
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
|
|
||||||
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
|
||||||
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
|
||||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/nwaples/rardecode v1.0.0 h1:r7vGuS5akxOnR4JQSkko62RJ1ReCMXxQRPtxsiFMBOs=
|
github.com/nwaples/rardecode v1.0.0 h1:r7vGuS5akxOnR4JQSkko62RJ1ReCMXxQRPtxsiFMBOs=
|
||||||
github.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
github.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||||
github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ=
|
|
||||||
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
|
||||||
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
|
|
||||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
|
||||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||||
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
|
||||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
|
||||||
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
|
||||||
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
|
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
|
||||||
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||||
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
|
|
||||||
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
|
|
||||||
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
|
||||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
|
||||||
github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
|
|
||||||
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
|
||||||
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
|
||||||
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
|
||||||
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
|
|
||||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
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/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
|
||||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
|
|
||||||
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
|
|
||||||
github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
|
github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
|
||||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||||
github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI=
|
|
||||||
github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
|
||||||
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
|
github.com/pkg/profile v1.4.0 h1:uCmaf4vVbWAOZz36k1hrQD7ijGRzLwaME8Am/7a4jZI=
|
||||||
github.com/pkg/profile v1.5.0 h1:042Buzk+NhDI+DeSAA62RwJL8VAuZUMQZUjCsRz1Mug=
|
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
|
||||||
github.com/pkg/profile v1.5.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
|
github.com/pkg/sftp v1.8.3 h1:9jSe2SxTM8/3bXZjtqnkgTBW+lA8db0knZJyns7gpBA=
|
||||||
|
github.com/pkg/sftp v1.8.3/go.mod h1:NxmoDg/QLVWluQDUYG7XBZTLUpKeFa8e3aMf1BfjyHk=
|
||||||
github.com/pkg/sftp v1.11.0 h1:4Zv0OGbpkg4yNuUtH0s8rvoYxRCNyT29NVUo6pgPmxI=
|
github.com/pkg/sftp v1.11.0 h1:4Zv0OGbpkg4yNuUtH0s8rvoYxRCNyT29NVUo6pgPmxI=
|
||||||
github.com/pkg/sftp v1.11.0/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
|
github.com/pkg/sftp v1.11.0/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
|
||||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
|
||||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||||
github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8=
|
github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8=
|
||||||
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
|
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
|
||||||
github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
|
|
||||||
github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA=
|
|
||||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
|
||||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
|
||||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
|
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
|
||||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
|
||||||
github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
|
||||||
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
|
|
||||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
|
||||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||||
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
|
||||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
github.com/prometheus/common v0.6.0 h1:kRhiuYSXR3+uv2IbVbZhUxK5zVD/2pp3Gd2PpvPkpEo=
|
github.com/prometheus/common v0.6.0 h1:kRhiuYSXR3+uv2IbVbZhUxK5zVD/2pp3Gd2PpvPkpEo=
|
||||||
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
|
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
|
||||||
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
|
|
||||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
|
||||||
github.com/prometheus/common v0.11.1 h1:0ZISXCMRuCZcxF77aT1BXY5m74mX2vrGYl1dSwBI0Jo=
|
|
||||||
github.com/prometheus/common v0.11.1/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
|
|
||||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
|
||||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
github.com/prometheus/procfs v0.0.3 h1:CTwfnzjQ+8dS6MhHHu4YswVAD99sL2wjPqP+VkURmKE=
|
github.com/prometheus/procfs v0.0.3 h1:CTwfnzjQ+8dS6MhHHu4YswVAD99sL2wjPqP+VkURmKE=
|
||||||
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
|
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
|
||||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
|
||||||
github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8=
|
|
||||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
|
||||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
github.com/pterodactyl/sftp-server v1.1.4 h1:JESuEuZ+d2tajMjuQblPOlGISM9Uc2xOzk7irVF9PQ0=
|
||||||
github.com/remeh/sizedwaitgroup v1.0.0 h1:VNGGFwNo/R5+MJBf6yrsr110p0m4/OX4S3DCy7Kyl5E=
|
github.com/pterodactyl/sftp-server v1.1.4/go.mod h1:KjSONrenRr1oCh94QIVAU6yEzMe+Hd7r/JHrh5/oQHs=
|
||||||
github.com/remeh/sizedwaitgroup v1.0.0/go.mod h1:3j2R4OIe/SeS6YDhICBy22RWjJC5eNCJ1V+9+NVNYlo=
|
github.com/remeh/sizedwaitgroup v0.0.0-20180822144253-5e7302b12cce h1:aP+C+YbHZfOQlutA4p4soHi7rVUqHQdWEVMSkHfDTqY=
|
||||||
|
github.com/remeh/sizedwaitgroup v0.0.0-20180822144253-5e7302b12cce/go.mod h1:3j2R4OIe/SeS6YDhICBy22RWjJC5eNCJ1V+9+NVNYlo=
|
||||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||||
github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
|
||||||
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94 h1:G04eS0JkAIVZfaJLjla9dNxkJCPiKIGZlw9AfOhzOD0=
|
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94 h1:G04eS0JkAIVZfaJLjla9dNxkJCPiKIGZlw9AfOhzOD0=
|
||||||
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94/go.mod h1:b18R55ulyQ/h3RaWyloPyER7fWQVZvimKKhnI5OfrJQ=
|
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94/go.mod h1:b18R55ulyQ/h3RaWyloPyER7fWQVZvimKKhnI5OfrJQ=
|
||||||
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
|
|
||||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
|
||||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
|
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
|
||||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
|
||||||
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
|
|
||||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
|
||||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
github.com/smartystreets/assertions v1.0.0 h1:UVQPSSmc3qtTi+zPPkCXvZX9VvW/xT/NsRvKfwY81a8=
|
github.com/smartystreets/assertions v1.0.0 h1:UVQPSSmc3qtTi+zPPkCXvZX9VvW/xT/NsRvKfwY81a8=
|
||||||
@@ -497,23 +297,15 @@ github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIK
|
|||||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs=
|
github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs=
|
||||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||||
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
|
||||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
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.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
github.com/spf13/cobra v0.0.7 h1:FfTH+vuMXOas8jmfb5/M7dzEYx7LpcLb7a0LPe34uOU=
|
||||||
github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
|
github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
|
||||||
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
|
|
||||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
|
||||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
|
||||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||||
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
|
||||||
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
|
|
||||||
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
@@ -521,19 +313,16 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
|||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
github.com/tj/assert v0.0.0-20171129193455-018094318fb0 h1:Rw8kxzWo1mr6FSaYXjQELRe88y2KdfynXdnK72rdjtA=
|
github.com/tj/assert v0.0.0-20171129193455-018094318fb0 h1:Rw8kxzWo1mr6FSaYXjQELRe88y2KdfynXdnK72rdjtA=
|
||||||
github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
|
github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
|
||||||
github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk=
|
|
||||||
github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk=
|
|
||||||
github.com/tj/go-buffer v1.1.0/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj52Uc=
|
|
||||||
github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
|
github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
|
||||||
github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
|
github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
|
||||||
github.com/tj/go-spin v1.1.0 h1:lhdWZsvImxvZ3q1C5OIB7d72DuOwP4O2NdBg9PyzNds=
|
github.com/tj/go-spin v1.1.0 h1:lhdWZsvImxvZ3q1C5OIB7d72DuOwP4O2NdBg9PyzNds=
|
||||||
github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
|
github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
|
||||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
|
||||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||||
|
github.com/uber-go/zap v1.9.1/go.mod h1:GY+83l3yxBcBw2kmHu/sAWwItnTn+ynxHCRo+WiIQOY=
|
||||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||||
@@ -541,189 +330,120 @@ github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs
|
|||||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||||
github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
|
github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
|
||||||
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
||||||
github.com/ulikunitz/xz v0.5.7 h1:YvTNdFzX6+W5m9msiYg/zpkSURPPtOlzbqYjrFn7Yt4=
|
|
||||||
github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
|
||||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
|
||||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
|
||||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
|
||||||
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
|
|
||||||
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
|
||||||
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
|
||||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
|
||||||
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
|
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
|
||||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
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=
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
|
||||||
|
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||||
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
|
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
|
||||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
|
||||||
|
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
|
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
|
||||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||||
|
go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o=
|
||||||
|
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM=
|
||||||
|
go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
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-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
|
||||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79 h1:IaQbIIB2X/Mp/DKctl6ROxz1KyMlKp4uyvL6+kQ7C88=
|
||||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig=
|
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
|
||||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
|
||||||
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
|
||||||
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/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=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
|
||||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 h1:Jcxah/M+oLZ/R4/z5RzfPzGbPXnVDPkEDtf2JnuxN+U=
|
||||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
|
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
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/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
|
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
|
||||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/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-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f h1:mOhmO9WsBaJCNmaZHPtHs9wOcdqdKCjF6OPJlmDM3KI=
|
||||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo=
|
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
golang.org/x/text v0.3.0/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.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/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-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
|
||||||
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s=
|
|
||||||
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
|
||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
|
||||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
|
||||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20190710153321-831012c29e42/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
|
||||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs=
|
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs=
|
||||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5 h1:MeC2gMlMdkd67dn17MEby3rGXRxZtWeiRXOnISfTQ74=
|
||||||
|
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools/gopls v0.1.3/go.mod h1:vrCQzOKxvuiZLjCKSmbbov04oeBQQOb4VQqwYK2PWIY=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
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=
|
|
||||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
|
||||||
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
|
|
||||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
|
||||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
|
||||||
google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98 h1:LCO0fg4kb6WwkXQXRQQgUYsFeFb5taTX5WAx5O/Vt28=
|
|
||||||
google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
|
||||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
|
|
||||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
|
||||||
google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0=
|
google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0=
|
||||||
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
|
||||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
|
||||||
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
|
||||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
|
||||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
|
||||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
|
||||||
google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI=
|
|
||||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
|
||||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
|
||||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
|
||||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
|
||||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
|
||||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
|
||||||
google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA=
|
|
||||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
|
||||||
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
|
||||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
@@ -731,34 +451,22 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
|
|||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
|
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
|
||||||
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
|
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
|
||||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
|
||||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
|
||||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c h1:grhR+C34yXImVGp7EzNk+DTIk+323eIUWOmEevy6bDo=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
||||||
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
|
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
|
||||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||||
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
honnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3U=
|
||||||
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
|
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/pterodactyl/wings/api"
|
"github.com/pterodactyl/wings/api"
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
"github.com/pterodactyl/wings/environment"
|
|
||||||
"github.com/pterodactyl/wings/server"
|
"github.com/pterodactyl/wings/server"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@@ -26,12 +25,15 @@ func New(data []byte) (*Installer, error) {
|
|||||||
return nil, NewValidationError("uuid provided was not in a valid format")
|
return nil, NewValidationError("uuid provided was not in a valid format")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !govalidator.IsUUIDv4(getString(data, "service", "egg")) {
|
||||||
|
return nil, NewValidationError("service egg provided was not in a valid format")
|
||||||
|
}
|
||||||
|
|
||||||
cfg := &server.Configuration{
|
cfg := &server.Configuration{
|
||||||
Uuid: getString(data, "uuid"),
|
Uuid: getString(data, "uuid"),
|
||||||
Suspended: false,
|
Suspended: false,
|
||||||
Invocation: getString(data, "invocation"),
|
Invocation: getString(data, "invocation"),
|
||||||
SkipEggScripts: getBoolean(data, "skip_egg_scripts"),
|
Build: server.BuildSettings{
|
||||||
Build: environment.Limits{
|
|
||||||
MemoryLimit: getInt(data, "build", "memory"),
|
MemoryLimit: getInt(data, "build", "memory"),
|
||||||
Swap: getInt(data, "build", "swap"),
|
Swap: getInt(data, "build", "swap"),
|
||||||
IoWeight: uint16(getInt(data, "build", "io")),
|
IoWeight: uint16(getInt(data, "build", "io")),
|
||||||
@@ -49,7 +51,7 @@ func New(data []byte) (*Installer, error) {
|
|||||||
if b, _, _, err := jsonparser.Get(data, "environment"); err != nil {
|
if b, _, _, err := jsonparser.Get(data, "environment"); err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
} else {
|
} else {
|
||||||
cfg.EnvVars = make(environment.Variables)
|
cfg.EnvVars = make(server.EnvironmentVariables)
|
||||||
if err := json.Unmarshal(b, &cfg.EnvVars); err != nil {
|
if err := json.Unmarshal(b, &cfg.EnvVars); err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
@@ -135,9 +137,3 @@ func getInt(data []byte, key ...string) int64 {
|
|||||||
|
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
func getBoolean(data []byte, key ...string) bool {
|
|
||||||
value, _ := jsonparser.GetBoolean(data, key...)
|
|
||||||
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Default = New(os.Stderr, true)
|
var Default = New(os.Stderr)
|
||||||
|
|
||||||
var bold = color2.New(color2.Bold)
|
var bold = color2.New(color2.Bold)
|
||||||
|
|
||||||
@@ -31,14 +31,12 @@ type Handler struct {
|
|||||||
Padding int
|
Padding int
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(w io.Writer, useColors bool) *Handler {
|
func New(w io.Writer) *Handler {
|
||||||
if f, ok := w.(*os.File); ok {
|
if f, ok := w.(*os.File); ok {
|
||||||
if useColors {
|
|
||||||
return &Handler{Writer: colorable.NewColorable(f), Padding: 2}
|
return &Handler{Writer: colorable.NewColorable(f), Padding: 2}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return &Handler{Writer: colorable.NewNonColorable(w), Padding: 2}
|
return &Handler{Writer: w, Padding: 2}
|
||||||
}
|
}
|
||||||
|
|
||||||
type tracer interface {
|
type tracer interface {
|
||||||
@@ -83,7 +81,7 @@ func (h *Handler) HandleLog(e *log.Entry) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getErrorStack(err error, i bool) errors.StackTrace {
|
func getErrorStack(err error, i bool) errors.StackTrace {
|
||||||
e, ok := err.(tracer)
|
e, ok := errors.Cause(err).(tracer)
|
||||||
if !ok {
|
if !ok {
|
||||||
if i {
|
if i {
|
||||||
// Just abort out of this and return a stacktrace leading up to this point. It isn't perfect
|
// Just abort out of this and return a stacktrace leading up to this point. It isn't perfect
|
||||||
@@ -91,7 +89,7 @@ func getErrorStack(err error, i bool) errors.StackTrace {
|
|||||||
return errors.Wrap(err, "failed to generate stacktrace for caught error").(tracer).StackTrace()
|
return errors.Wrap(err, "failed to generate stacktrace for caught error").(tracer).StackTrace()
|
||||||
}
|
}
|
||||||
|
|
||||||
return getErrorStack(errors.Wrap(err, err.Error()), true)
|
return getErrorStack(errors.New(err.Error()), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
st := e.StackTrace()
|
st := e.StackTrace()
|
||||||
|
|||||||
@@ -76,13 +76,13 @@ func (cfr *ConfigurationFileReplacement) getKeyValue(value []byte) interface{} {
|
|||||||
func (f *ConfigurationFile) IterateOverJson(data []byte) (*gabs.Container, error) {
|
func (f *ConfigurationFile) IterateOverJson(data []byte) (*gabs.Container, error) {
|
||||||
parsed, err := gabs.ParseJSON(data)
|
parsed, err := gabs.ParseJSON(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range f.Replace {
|
for _, v := range f.Replace {
|
||||||
value, err := f.LookupConfigurationValue(v)
|
value, err := f.LookupConfigurationValue(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a wildcard character, and if found split the key on that value to
|
// Check for a wildcard character, and if found split the key on that value to
|
||||||
@@ -97,20 +97,12 @@ func (f *ConfigurationFile) IterateOverJson(data []byte) (*gabs.Container, error
|
|||||||
// time this code is being written.
|
// time this code is being written.
|
||||||
for _, child := range parsed.Path(strings.Trim(parts[0], ".")).Children() {
|
for _, child := range parsed.Path(strings.Trim(parts[0], ".")).Children() {
|
||||||
if err := v.SetAtPathway(child, strings.Trim(parts[1], "."), []byte(value)); err != nil {
|
if err := v.SetAtPathway(child, strings.Trim(parts[1], "."), []byte(value)); err != nil {
|
||||||
if errors.Is(err, gabs.ErrNotFound) {
|
return nil, err
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, errors.Wrap(err, "failed to set config value of array child")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err = v.SetAtPathway(parsed, v.Match, []byte(value)); err != nil {
|
if err = v.SetAtPathway(parsed, v.Match, []byte(value)); err != nil {
|
||||||
if errors.Is(err, gabs.ErrNotFound) {
|
return nil, err
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, errors.Wrap(err, "unable to set config value at pathway: "+v.Match)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,91 +110,13 @@ func (f *ConfigurationFile) IterateOverJson(data []byte) (*gabs.Container, error
|
|||||||
return parsed, nil
|
return parsed, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regex used to check if there is an array element present in the given pathway by looking for something
|
|
||||||
// along the lines of "something[1]" or "something[1].nestedvalue" as the path.
|
|
||||||
var checkForArrayElement = regexp.MustCompile(`^([^\[\]]+)\[([\d]+)](\..+)?$`)
|
|
||||||
|
|
||||||
// Attempt to set the value of the path depending on if it is an array or not. Gabs cannot handle array
|
|
||||||
// values as "something[1]" but can parse them just fine. This is basically just overly complex code
|
|
||||||
// to handle that edge case and ensure the value gets set correctly.
|
|
||||||
//
|
|
||||||
// Bless thee who has to touch these most unholy waters.
|
|
||||||
func setValueAtPath(c *gabs.Container, path string, value interface{}) error {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
matches := checkForArrayElement.FindStringSubmatch(path)
|
|
||||||
if len(matches) < 3 {
|
|
||||||
// Only update the value if the pathway actually exists in the configuration, otherwise
|
|
||||||
// do nothing.
|
|
||||||
if c.ExistsP(path) {
|
|
||||||
_, err = c.SetP(value, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
i, _ := strconv.Atoi(matches[2])
|
|
||||||
// Find the array element "i" or try to create it if "i" is equal to 0 and is not found
|
|
||||||
// at the given path.
|
|
||||||
ct, err := c.ArrayElementP(i, matches[1])
|
|
||||||
if err != nil {
|
|
||||||
if i != 0 || (!errors.Is(err, gabs.ErrNotArray) && !errors.Is(err, gabs.ErrNotFound)) {
|
|
||||||
return errors.Wrap(err, "error while parsing array element at path")
|
|
||||||
}
|
|
||||||
|
|
||||||
var t = make([]interface{}, 1)
|
|
||||||
// If the length of matches is 4 it means we're trying to access an object down in this array
|
|
||||||
// key, so make sure we generate the array as an array of objects, and not just a generic nil
|
|
||||||
// array.
|
|
||||||
if len(matches) == 4 {
|
|
||||||
t = []interface{}{map[string]interface{}{}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the error is because this isn't an array or isn't found go ahead and create the array with
|
|
||||||
// an empty object if we have additional things to set on the array, or just an empty array type
|
|
||||||
// if there is not an object structure detected (no matches[3] available).
|
|
||||||
if _, err = c.SetP(t, matches[1]); err != nil {
|
|
||||||
return errors.Wrap(err, "failed to create empty array for missing element")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set our cursor to be the array element we expect, which in this case is just the first element
|
|
||||||
// since we won't run this code unless the array element is 0. There is too much complexity in trying
|
|
||||||
// to match additional elements. In those cases the server will just have to be rebooted or something.
|
|
||||||
ct, err = c.ArrayElementP(0, matches[1])
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to find array element at path")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to set the value. If the path does not exist an error will be raised to the caller which will
|
|
||||||
// then check if the error is because the path is missing. In those cases we just ignore the error since
|
|
||||||
// we don't want to do anything specifically when that happens.
|
|
||||||
//
|
|
||||||
// If there are four matches in the regex it means that we managed to also match a trailing pathway
|
|
||||||
// for the key, which should be found in the given array key item and modified further.
|
|
||||||
if len(matches) == 4 {
|
|
||||||
_, err = ct.SetP(value, strings.TrimPrefix(matches[3], "."))
|
|
||||||
} else {
|
|
||||||
_, err = ct.Set(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to set value at config path: "+path)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sets the value at a specific pathway, but checks if we were looking for a specific
|
// Sets the value at a specific pathway, but checks if we were looking for a specific
|
||||||
// value or not before doing it.
|
// value or not before doing it.
|
||||||
func (cfr *ConfigurationFileReplacement) SetAtPathway(c *gabs.Container, path string, value []byte) error {
|
func (cfr *ConfigurationFileReplacement) SetAtPathway(c *gabs.Container, path string, value []byte) error {
|
||||||
if cfr.IfValue == "" {
|
if cfr.IfValue != "" {
|
||||||
return setValueAtPath(c, path, cfr.getKeyValue(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this is a regex based matching, we need to get a little more creative since
|
// If this is a regex based matching, we need to get a little more creative since
|
||||||
// we're only going to replacing part of the string, and not the whole thing.
|
// we're only going to replacing part of the string, and not the whole thing.
|
||||||
if c.ExistsP(path) && strings.HasPrefix(cfr.IfValue, "regex:") {
|
if c.Exists(path) && strings.HasPrefix(cfr.IfValue, "regex:") {
|
||||||
// We're doing some regex here.
|
// We're doing some regex here.
|
||||||
r, err := regexp.Compile(strings.TrimPrefix(cfr.IfValue, "regex:"))
|
r, err := regexp.Compile(strings.TrimPrefix(cfr.IfValue, "regex:"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -216,15 +130,22 @@ func (cfr *ConfigurationFileReplacement) SetAtPathway(c *gabs.Container, path st
|
|||||||
// using the value we got from the key. This will only replace the one match.
|
// using the value we got from the key. This will only replace the one match.
|
||||||
v := strings.Trim(string(c.Path(path).Bytes()), "\"")
|
v := strings.Trim(string(c.Path(path).Bytes()), "\"")
|
||||||
if r.Match([]byte(v)) {
|
if r.Match([]byte(v)) {
|
||||||
return setValueAtPath(c, path, r.ReplaceAllString(v, string(value)))
|
_, err := c.SetP(r.ReplaceAllString(v, string(value)), path)
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
} else if !c.ExistsP(path) || (c.ExistsP(path) && !bytes.Equal(c.Bytes(), []byte(cfr.IfValue))) {
|
} else {
|
||||||
|
if !c.Exists(path) || (c.Exists(path) && !bytes.Equal(c.Bytes(), []byte(cfr.IfValue))) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return setValueAtPath(c, path, cfr.getKeyValue(value))
|
_, err := c.SetP(cfr.getKeyValue(value), path)
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Looks up a configuration value on the Daemon given a dot-notated syntax.
|
// Looks up a configuration value on the Daemon given a dot-notated syntax.
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -96,7 +95,8 @@ func (cfr *ConfigurationFileReplacement) UnmarshalJSON(data []byte) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cfr.Match = m
|
// See comment on the replacement regex to understand what exactly this is doing.
|
||||||
|
cfr.Match = cfrMatchReplacement.ReplaceAllString(m, ".$1")
|
||||||
|
|
||||||
iv, err := jsonparser.GetString(data, "if_value")
|
iv, err := jsonparser.GetString(data, "if_value")
|
||||||
// We only check keypath here since match & replace_with should be present on all of
|
// We only check keypath here since match & replace_with should be present on all of
|
||||||
@@ -162,7 +162,7 @@ func (f *ConfigurationFile) Parse(path string, internal bool) error {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
if os.IsNotExist(err) {
|
||||||
// File doesn't exist, we tried creating it, and same error is returned? Pretty
|
// File doesn't exist, we tried creating it, and same error is returned? Pretty
|
||||||
// sure this pathway is impossible, but if not, abort here.
|
// sure this pathway is impossible, but if not, abort here.
|
||||||
if internal {
|
if internal {
|
||||||
@@ -348,33 +348,33 @@ func (f *ConfigurationFile) parseJsonFile(path string) error {
|
|||||||
func (f *ConfigurationFile) parseYamlFile(path string) error {
|
func (f *ConfigurationFile) parseYamlFile(path string) error {
|
||||||
b, err := readFileBytes(path)
|
b, err := readFileBytes(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
i := make(map[string]interface{})
|
i := make(map[string]interface{})
|
||||||
if err := yaml.Unmarshal(b, &i); err != nil {
|
if err := yaml.Unmarshal(b, &i); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unmarshal the yaml data into a JSON interface such that we can work with
|
// Unmarshal the yaml data into a JSON interface such that we can work with
|
||||||
// any arbitrary data structure. If we don't do this, I can't use gabs which
|
// any arbitrary data structure. If we don't do this, I can't use gabs which
|
||||||
// makes working with unknown JSON significantly easier.
|
// makes working with unknown JSON signficiantly easier.
|
||||||
jsonBytes, err := json.Marshal(dyno.ConvertMapI2MapS(i))
|
jsonBytes, err := json.Marshal(dyno.ConvertMapI2MapS(i))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now that the data is converted, treat it just like JSON and pass it to the
|
// Now that the data is converted, treat it just like JSON and pass it to the
|
||||||
// iterator function to update values as necessary.
|
// iterator function to update values as necessary.
|
||||||
data, err := f.IterateOverJson(jsonBytes)
|
data, err := f.IterateOverJson(jsonBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remarshal the JSON into YAML format before saving it back to the disk.
|
// Remarshal the JSON into YAML format before saving it back to the disk.
|
||||||
marshaled, err := yaml.Marshal(data.Data())
|
marshaled, err := yaml.Marshal(data.Data())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return ioutil.WriteFile(path, marshaled, 0644)
|
return ioutil.WriteFile(path, marshaled, 0644)
|
||||||
@@ -425,46 +425,15 @@ func (f *ConfigurationFile) parseTextFile(path string) error {
|
|||||||
// Parses a properties file and updates the values within it to match those that
|
// Parses a properties file and updates the values within it to match those that
|
||||||
// are passed. Writes the file once completed.
|
// are passed. Writes the file once completed.
|
||||||
func (f *ConfigurationFile) parsePropertiesFile(path string) error {
|
func (f *ConfigurationFile) parsePropertiesFile(path string) error {
|
||||||
// Open the file.
|
|
||||||
f2, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var s strings.Builder
|
|
||||||
|
|
||||||
// Get any header comments from the file.
|
|
||||||
scanner := bufio.NewScanner(f2)
|
|
||||||
for scanner.Scan() {
|
|
||||||
text := scanner.Text()
|
|
||||||
|
|
||||||
if text[0] != '#' {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
s.WriteString(text)
|
|
||||||
s.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the file.
|
|
||||||
_ = f2.Close()
|
|
||||||
|
|
||||||
// Handle any scanner errors.
|
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode the properties file.
|
|
||||||
p, err := properties.LoadFile(path, properties.UTF8)
|
p, err := properties.LoadFile(path, properties.UTF8)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace any values that need to be replaced.
|
|
||||||
for _, replace := range f.Replace {
|
for _, replace := range f.Replace {
|
||||||
data, err := f.LookupConfigurationValue(replace)
|
data, err := f.LookupConfigurationValue(replace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
v, ok := p.Get(replace.Match)
|
v, ok := p.Get(replace.Match)
|
||||||
@@ -476,34 +445,16 @@ func (f *ConfigurationFile) parsePropertiesFile(path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if _, _, err := p.Set(replace.Match, data); err != nil {
|
if _, _, err := p.Set(replace.Match, data); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the new file content to the string builder.
|
|
||||||
for _, key := range p.Keys() {
|
|
||||||
value, ok := p.Get(key)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
s.WriteString(key)
|
|
||||||
s.WriteByte('=')
|
|
||||||
s.WriteString(strings.Trim(strconv.QuoteToASCII(value), `"`))
|
|
||||||
s.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open the file for writing.
|
|
||||||
w, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
w, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
defer w.Close()
|
|
||||||
|
|
||||||
// Write the data to the file.
|
|
||||||
if _, err := w.Write([]byte(s.String())); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
_, err = p.Write(w, properties.UTF8)
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,22 +11,8 @@ import (
|
|||||||
|
|
||||||
// Set the access request control headers on all of the requests.
|
// Set the access request control headers on all of the requests.
|
||||||
func SetAccessControlHeaders(c *gin.Context) {
|
func SetAccessControlHeaders(c *gin.Context) {
|
||||||
c.Header("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
|
|
||||||
|
|
||||||
o := c.GetHeader("Origin")
|
|
||||||
if o != config.Get().PanelLocation {
|
|
||||||
for _, origin := range config.Get().AllowedOrigins {
|
|
||||||
if origin != "*" && o != origin {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Header("Access-Control-Allow-Origin", origin)
|
|
||||||
c.Next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Header("Access-Control-Allow-Origin", config.Get().PanelLocation)
|
c.Header("Access-Control-Allow-Origin", config.Get().PanelLocation)
|
||||||
|
c.Header("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
|
||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ func Configure() *gin.Engine {
|
|||||||
// These routes use signed URLs to validate access to the resource being requested.
|
// These routes use signed URLs to validate access to the resource being requested.
|
||||||
router.GET("/download/backup", getDownloadBackup)
|
router.GET("/download/backup", getDownloadBackup)
|
||||||
router.GET("/download/file", getDownloadFile)
|
router.GET("/download/file", getDownloadFile)
|
||||||
router.POST("/upload/file", postServerUploadFiles)
|
|
||||||
|
|
||||||
// This route is special it sits above all of the other requests because we are
|
// This route is special it sits above all of the other requests because we are
|
||||||
// using a JWT to authorize access to it, therefore it needs to be publicly
|
// using a JWT to authorize access to it, therefore it needs to be publicly
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package router
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"errors"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/pterodactyl/wings/router/tokens"
|
"github.com/pterodactyl/wings/router/tokens"
|
||||||
"github.com/pterodactyl/wings/server/backup"
|
"github.com/pterodactyl/wings/server/backup"
|
||||||
@@ -29,7 +28,7 @@ func getDownloadBackup(c *gin.Context) {
|
|||||||
|
|
||||||
b, st, err := backup.LocateLocal(token.BackupUuid)
|
b, st, err := backup.LocateLocal(token.BackupUuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
if os.IsNotExist(err) {
|
||||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
|
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
|
||||||
"error": "The requested backup was not found on this server.",
|
"error": "The requested backup was not found on this server.",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package router
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -12,30 +11,18 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
type serverProcData struct {
|
|
||||||
server.ResourceUsage
|
|
||||||
Suspended bool `json:"suspended"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns a single server from the collection of servers.
|
// Returns a single server from the collection of servers.
|
||||||
func getServer(c *gin.Context) {
|
func getServer(c *gin.Context) {
|
||||||
s := GetServer(c.Param("server"))
|
c.JSON(http.StatusOK, GetServer(c.Param("server")))
|
||||||
|
|
||||||
c.JSON(http.StatusOK, serverProcData{
|
|
||||||
ResourceUsage: *s.Proc(),
|
|
||||||
Suspended: s.IsSuspended(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the logs for a given server instance.
|
// Returns the logs for a given server instance.
|
||||||
func getServerLogs(c *gin.Context) {
|
func getServerLogs(c *gin.Context) {
|
||||||
s := GetServer(c.Param("server"))
|
s := GetServer(c.Param("server"))
|
||||||
|
|
||||||
l, _ := strconv.Atoi(c.DefaultQuery("size", "100"))
|
l, _ := strconv.ParseInt(c.DefaultQuery("size", "8192"), 10, 64)
|
||||||
if l <= 0 {
|
if l <= 0 {
|
||||||
l = 100
|
l = 2048
|
||||||
} else if l > 100 {
|
|
||||||
l = 100
|
|
||||||
}
|
}
|
||||||
|
|
||||||
out, err := s.ReadLogfile(l)
|
out, err := s.ReadLogfile(l)
|
||||||
@@ -58,15 +45,13 @@ func getServerLogs(c *gin.Context) {
|
|||||||
func postServerPower(c *gin.Context) {
|
func postServerPower(c *gin.Context) {
|
||||||
s := GetServer(c.Param("server"))
|
s := GetServer(c.Param("server"))
|
||||||
|
|
||||||
var data struct {
|
var data server.PowerAction
|
||||||
Action server.PowerAction `json:"action"`
|
// BindJSON sends 400 if the request fails, all we need to do is return
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.BindJSON(&data); err != nil {
|
if err := c.BindJSON(&data); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !data.Action.IsValid() {
|
if !data.IsValid() {
|
||||||
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
|
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
|
||||||
"error": "The power action provided was not valid, should be one of \"stop\", \"start\", \"restart\", \"kill\"",
|
"error": "The power action provided was not valid, should be one of \"stop\", \"start\", \"restart\", \"kill\"",
|
||||||
})
|
})
|
||||||
@@ -79,26 +64,21 @@ func postServerPower(c *gin.Context) {
|
|||||||
//
|
//
|
||||||
// We don't really care about any of the other actions at this point, they'll all result
|
// We don't really care about any of the other actions at this point, they'll all result
|
||||||
// in the process being stopped, which should have happened anyways if the server is suspended.
|
// in the process being stopped, which should have happened anyways if the server is suspended.
|
||||||
if (data.Action == server.PowerActionStart || data.Action == server.PowerActionRestart) && s.IsSuspended() {
|
if (data.Action == "start" || data.Action == "restart") && s.IsSuspended() {
|
||||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
||||||
"error": "Cannot start or restart a server that is suspended.",
|
"error": "Cannot start or restart a server that is suspended.",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass the actual heavy processing off to a separate thread to handle so that
|
// Pass the actual heavy processing off to a seperate thread to handle so that
|
||||||
// we can immediately return a response from the server. Some of these actions
|
// we can immediately return a response from the server. Some of these actions
|
||||||
// can take quite some time, especially stopping or restarting.
|
// can take quite some time, especially stopping or restarting.
|
||||||
go func(s *server.Server) {
|
go func(server *server.Server) {
|
||||||
if err := s.HandlePowerAction(data.Action, 30); err != nil {
|
if err := server.HandlePowerAction(data); err != nil {
|
||||||
if errors.Is(err, context.DeadlineExceeded) {
|
server.Log().WithFields(log.Fields{"action": data, "error": err}).
|
||||||
s.Log().WithField("action", data.Action).
|
|
||||||
Warn("could not acquire a lock while attempting to perform a power action")
|
|
||||||
} else {
|
|
||||||
s.Log().WithFields(log.Fields{"action": data, "error": err}).
|
|
||||||
Error("encountered error processing a server power action in the background")
|
Error("encountered error processing a server power action in the background")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}(s)
|
}(s)
|
||||||
|
|
||||||
c.Status(http.StatusAccepted)
|
c.Status(http.StatusAccepted)
|
||||||
@@ -142,13 +122,11 @@ func patchServer(c *gin.Context) {
|
|||||||
buf := bytes.Buffer{}
|
buf := bytes.Buffer{}
|
||||||
buf.ReadFrom(c.Request.Body)
|
buf.ReadFrom(c.Request.Body)
|
||||||
|
|
||||||
if err := s.UpdateDataStructure(buf.Bytes()); err != nil {
|
if err := s.UpdateDataStructure(buf.Bytes(), true); err != nil {
|
||||||
TrackedServerError(err, s).AbortWithServerError(c)
|
TrackedServerError(err, s).AbortWithServerError(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.SyncWithEnvironment()
|
|
||||||
|
|
||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +156,7 @@ func postServerReinstall(c *gin.Context) {
|
|||||||
c.Status(http.StatusAccepted)
|
c.Status(http.StatusAccepted)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deletes a server from the wings daemon and dissociate it's objects.
|
// Deletes a server from the wings daemon and deassociates its objects.
|
||||||
func deleteServer(c *gin.Context) {
|
func deleteServer(c *gin.Context) {
|
||||||
s := GetServer(c.Param("server"))
|
s := GetServer(c.Param("server"))
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"github.com/pterodactyl/wings/server"
|
"github.com/pterodactyl/wings/server"
|
||||||
"github.com/pterodactyl/wings/server/backup"
|
"github.com/pterodactyl/wings/server/backup"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Backs up a server.
|
// Backs up a server.
|
||||||
@@ -47,35 +46,20 @@ func postServerBackup(c *gin.Context) {
|
|||||||
c.Status(http.StatusAccepted)
|
c.Status(http.StatusAccepted)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deletes a local backup of a server. If the backup is not found on the machine just return
|
// Deletes a local backup of a server.
|
||||||
// a 404 error. The service calling this endpoint can make its own decisions as to how it wants
|
|
||||||
// to handle that response.
|
|
||||||
func deleteServerBackup(c *gin.Context) {
|
func deleteServerBackup(c *gin.Context) {
|
||||||
s := GetServer(c.Param("server"))
|
s := GetServer(c.Param("server"))
|
||||||
|
|
||||||
b, _, err := backup.LocateLocal(c.Param("backup"))
|
b, _, err := backup.LocateLocal(c.Param("backup"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Just return from the function at this point if the backup was not located.
|
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
|
||||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
|
|
||||||
"error": "The requested backup was not found on this server.",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
TrackedServerError(err, s).AbortWithServerError(c)
|
TrackedServerError(err, s).AbortWithServerError(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := b.Remove(); err != nil {
|
if err := b.Remove(); err != nil {
|
||||||
// I'm not entirely sure how likely this is to happen, however if we did manage to locate
|
|
||||||
// the backup previously and it is now missing when we go to delete, just treat it as having
|
|
||||||
// been successful, rather than returning a 404.
|
|
||||||
if !errors.Is(err, os.ErrNotExist) {
|
|
||||||
TrackedServerError(err, s).AbortWithServerError(c)
|
TrackedServerError(err, s).AbortWithServerError(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,16 +4,12 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/pterodactyl/wings/router/tokens"
|
|
||||||
"github.com/pterodactyl/wings/server"
|
"github.com/pterodactyl/wings/server"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"mime/multipart"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -133,29 +129,12 @@ func putServerRenameFiles(c *gin.Context) {
|
|||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
default:
|
default:
|
||||||
if err := s.Filesystem.Rename(pf, pt); err != nil {
|
return s.Filesystem.Rename(pf, pt)
|
||||||
// Return nil if the error is an is not exists.
|
|
||||||
// NOTE: os.IsNotExist() does not work if the error is wrapped.
|
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := g.Wait(); err != nil {
|
if err := g.Wait(); err != nil {
|
||||||
if errors.Is(err, os.ErrExist) {
|
|
||||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
|
||||||
"error": "Cannot move or rename file, destination already exists.",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
TrackedServerError(err, s).AbortWithServerError(c)
|
TrackedServerError(err, s).AbortWithServerError(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -176,13 +155,6 @@ func postServerCopyFile(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Filesystem.Copy(data.Location); err != nil {
|
if err := s.Filesystem.Copy(data.Location); err != nil {
|
||||||
// Check if the file does not exist.
|
|
||||||
// NOTE: os.IsNotExist() does not work if the error is wrapped.
|
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
|
||||||
c.Status(http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
TrackedServerError(err, s).AbortWithServerError(c)
|
TrackedServerError(err, s).AbortWithServerError(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -205,7 +177,7 @@ func postServerDeleteFiles(c *gin.Context) {
|
|||||||
|
|
||||||
if len(data.Files) == 0 {
|
if len(data.Files) == 0 {
|
||||||
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
|
c.AbortWithStatusJSON(http.StatusUnprocessableEntity, gin.H{
|
||||||
"error": "No files were specified for deletion.",
|
"error": "No files were specififed for deletion.",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -294,7 +266,7 @@ func postServerCompressFiles(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !s.Filesystem.HasSpaceAvailable(true) {
|
if !s.Filesystem.HasSpaceAvailable() {
|
||||||
c.AbortWithStatusJSON(http.StatusConflict, gin.H{
|
c.AbortWithStatusJSON(http.StatusConflict, gin.H{
|
||||||
"error": "This server does not have enough available disk space to generate a compressed archive.",
|
"error": "This server does not have enough available disk space to generate a compressed archive.",
|
||||||
})
|
})
|
||||||
@@ -339,86 +311,9 @@ func postServerDecompressFiles(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Filesystem.DecompressFile(data.RootPath, data.File); err != nil {
|
if err := s.Filesystem.DecompressFile(data.RootPath, data.File); err != nil {
|
||||||
// Check if the file does not exist.
|
|
||||||
// NOTE: os.IsNotExist() does not work if the error is wrapped.
|
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
|
||||||
c.Status(http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
TrackedServerError(err, s).AbortWithServerError(c)
|
TrackedServerError(err, s).AbortWithServerError(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
func postServerUploadFiles(c *gin.Context) {
|
|
||||||
token := tokens.UploadPayload{}
|
|
||||||
if err := tokens.ParseToken([]byte(c.Query("token")), &token); err != nil {
|
|
||||||
TrackedError(err).AbortWithServerError(c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s := GetServer(token.ServerUuid)
|
|
||||||
if s == nil || !token.IsUniqueRequest() {
|
|
||||||
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
|
|
||||||
"error": "The requested resource was not found on this server.",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !s.Filesystem.HasSpaceAvailable(true) {
|
|
||||||
c.AbortWithStatusJSON(http.StatusConflict, gin.H{
|
|
||||||
"error": "This server does not have enough available disk space to accept any file uploads.",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
form, err := c.MultipartForm()
|
|
||||||
if err != nil {
|
|
||||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
|
||||||
"error": "Failed to get multipart form data from request.",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
headers, ok := form.File["files"]
|
|
||||||
if !ok {
|
|
||||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
|
||||||
"error": "No files were found on the request body.",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
directory := c.Query("directory")
|
|
||||||
|
|
||||||
for _, header := range headers {
|
|
||||||
p, err := s.Filesystem.SafePath(filepath.Join(directory, header.Filename))
|
|
||||||
if err != nil {
|
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// We run this in a different method so I can use defer without any of
|
|
||||||
// the consequences caused by calling it in a loop.
|
|
||||||
if err := handleFileUpload(p, s, header); err != nil {
|
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleFileUpload(p string, s *server.Server, header *multipart.FileHeader) error {
|
|
||||||
file, err := header.Open()
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
if err := s.Filesystem.Writefile(p, file); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ func postCreateServer(c *gin.Context) {
|
|||||||
func postUpdateConfiguration(c *gin.Context) {
|
func postUpdateConfiguration(c *gin.Context) {
|
||||||
// A backup of the configuration for error purposes.
|
// A backup of the configuration for error purposes.
|
||||||
ccopy := *config.Get()
|
ccopy := *config.Get()
|
||||||
// A copy of the configuration we're using to bind the data received into.
|
// A copy of the configuration we're using to bind the data recevied into.
|
||||||
cfg := *config.Get()
|
cfg := *config.Get()
|
||||||
|
|
||||||
// BindJSON sends 400 if the request fails, all we need to do is return
|
// BindJSON sends 400 if the request fails, all we need to do is return
|
||||||
|
|||||||
@@ -5,16 +5,16 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"github.com/apex/log"
|
"errors"
|
||||||
"github.com/buger/jsonparser"
|
"github.com/buger/jsonparser"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/mholt/archiver/v3"
|
"github.com/mholt/archiver/v3"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/pterodactyl/wings/api"
|
"github.com/pterodactyl/wings/api"
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
"github.com/pterodactyl/wings/installer"
|
"github.com/pterodactyl/wings/installer"
|
||||||
"github.com/pterodactyl/wings/router/tokens"
|
"github.com/pterodactyl/wings/router/tokens"
|
||||||
"github.com/pterodactyl/wings/server"
|
"github.com/pterodactyl/wings/server"
|
||||||
|
"go.uber.org/zap"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getServerArchive(c *gin.Context) {
|
func getServerArchive(c *gin.Context) {
|
||||||
@@ -93,34 +94,45 @@ func getServerArchive(c *gin.Context) {
|
|||||||
func postServerArchive(c *gin.Context) {
|
func postServerArchive(c *gin.Context) {
|
||||||
s := GetServer(c.Param("server"))
|
s := GetServer(c.Param("server"))
|
||||||
|
|
||||||
go func(s *server.Server) {
|
go func(server *server.Server) {
|
||||||
if err := s.Archiver.Archive(); err != nil {
|
start := time.Now()
|
||||||
s.Log().WithField("error", err).Error("failed to get archive for server")
|
|
||||||
|
if err := server.Archiver.Archive(); err != nil {
|
||||||
|
zap.S().Errorw("failed to get archive for server", zap.String("server", server.Id()), zap.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Log().Debug("successfully created server archive, notifying panel")
|
zap.S().Debugw(
|
||||||
|
"successfully created archive for server",
|
||||||
|
zap.String("server", server.Id()),
|
||||||
|
zap.Duration("time", time.Now().Sub(start).Round(time.Microsecond)),
|
||||||
|
)
|
||||||
|
|
||||||
r := api.NewRequester()
|
r := api.NewRequester()
|
||||||
rerr, err := r.SendArchiveStatus(s.Id(), true)
|
rerr, err := r.SendArchiveStatus(server.Id(), true)
|
||||||
if rerr != nil || err != nil {
|
if rerr != nil || err != nil {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log().WithField("error", err).Error("failed to notify panel of archive status")
|
zap.S().Errorw("failed to notify panel with archive status", zap.String("server", server.Id()), zap.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Log().WithField("error", rerr.String()).Error("panel returned an error when sending the archive status")
|
zap.S().Errorw(
|
||||||
|
"panel returned an error when sending the archive status",
|
||||||
|
zap.String("server", server.Id()),
|
||||||
|
zap.Error(errors.New(rerr.String())),
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Log().Debug("successfully notified panel of archive status")
|
zap.S().Debugw("successfully notified panel about archive status", zap.String("server", server.Id()))
|
||||||
}(s)
|
}(s)
|
||||||
|
|
||||||
c.Status(http.StatusAccepted)
|
c.Status(http.StatusAccepted)
|
||||||
}
|
}
|
||||||
|
|
||||||
func postTransfer(c *gin.Context) {
|
func postTransfer(c *gin.Context) {
|
||||||
|
zap.S().Debug("incoming transfer from panel")
|
||||||
|
|
||||||
buf := bytes.Buffer{}
|
buf := bytes.Buffer{}
|
||||||
buf.ReadFrom(c.Request.Body)
|
buf.ReadFrom(c.Request.Body)
|
||||||
|
|
||||||
@@ -129,7 +141,6 @@ func postTransfer(c *gin.Context) {
|
|||||||
url, _ := jsonparser.GetString(data, "url")
|
url, _ := jsonparser.GetString(data, "url")
|
||||||
token, _ := jsonparser.GetString(data, "token")
|
token, _ := jsonparser.GetString(data, "token")
|
||||||
|
|
||||||
l := log.WithField("server", serverID)
|
|
||||||
// Create an http client with no timeout.
|
// Create an http client with no timeout.
|
||||||
client := &http.Client{Timeout: 0}
|
client := &http.Client{Timeout: 0}
|
||||||
|
|
||||||
@@ -139,25 +150,25 @@ func postTransfer(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Info("server transfer failed, notifying panel")
|
zap.S().Errorw("server transfer has failed", zap.String("server", serverID))
|
||||||
rerr, err := api.NewRequester().SendTransferFailure(serverID)
|
rerr, err := api.NewRequester().SendTransferFailure(serverID)
|
||||||
if rerr != nil || err != nil {
|
if rerr != nil || err != nil {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithField("error", err).Error("failed to notify panel with transfer failure")
|
zap.S().Errorw("failed to notify panel with transfer failure", zap.String("server", serverID), zap.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
l.WithField("error", errors.WithStack(rerr)).Error("received error response from panel while notifying of transfer failure")
|
zap.S().Errorw("panel returned an error when notifying of a transfer failure", zap.String("server", serverID), zap.Error(errors.New(rerr.String())))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Debug("notified panel of transfer failure")
|
zap.S().Debugw("successfully notified panel about transfer failure", zap.String("server", serverID))
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Make a new GET request to the URL the panel gave us.
|
// Make a new GET request to the URL the panel gave us.
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithField("error", errors.WithStack(err)).Error("failed to create http request for archive transfer")
|
zap.S().Errorw("failed to create http request", zap.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,39 +178,36 @@ func postTransfer(c *gin.Context) {
|
|||||||
// Execute the http request.
|
// Execute the http request.
|
||||||
res, err := client.Do(req)
|
res, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithField("error", errors.WithStack(err)).Error("failed to send archive http request")
|
zap.S().Errorw("failed to send http request", zap.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
// Handle non-200 status codes.
|
// Handle non-200 status codes.
|
||||||
if res.StatusCode != 200 {
|
if res.StatusCode != 200 {
|
||||||
_, err := ioutil.ReadAll(res.Body)
|
body, err := ioutil.ReadAll(res.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithField("error", errors.WithStack(err)).WithField("status", res.StatusCode).Error("failed read transfer response body")
|
zap.S().Errorw("failed to read response body", zap.Int("status", res.StatusCode), zap.Error(err))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
l.WithField("error", errors.WithStack(err)).WithField("status", res.StatusCode).Error("failed to request server archive")
|
zap.S().Errorw("failed to request server archive", zap.Int("status", res.StatusCode), zap.String("body", string(body)))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the path to the archive.
|
// Get the path to the archive.
|
||||||
archivePath := filepath.Join(config.Get().System.ArchiveDirectory, serverID+".tar.gz")
|
archivePath := filepath.Join(config.Get().System.ArchiveDirectory, serverID + ".tar.gz")
|
||||||
|
|
||||||
// Check if the archive already exists and delete it if it does.
|
// Check if the archive already exists and delete it if it does.
|
||||||
_, err = os.Stat(archivePath)
|
_, err = os.Stat(archivePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !os.IsNotExist(err) {
|
if !os.IsNotExist(err) {
|
||||||
l.WithField("error", errors.WithStack(err)).Error("failed to stat archive file")
|
zap.S().Errorw("failed to stat file", zap.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := os.Remove(archivePath); err != nil {
|
if err := os.Remove(archivePath); err != nil {
|
||||||
l.WithField("error", errors.WithStack(err)).Warn("failed to remove old archive file")
|
zap.S().Errorw("failed to delete old file", zap.Error(err))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,69 +215,63 @@ func postTransfer(c *gin.Context) {
|
|||||||
// Create the file.
|
// Create the file.
|
||||||
file, err := os.Create(archivePath)
|
file, err := os.Create(archivePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithField("error", errors.WithStack(err)).Error("failed to open archive on disk")
|
zap.S().Errorw("failed to open file on disk", zap.Error(err))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy the file.
|
// Copy the file.
|
||||||
buf := make([]byte, 1024*4)
|
_, err = io.Copy(file, res.Body)
|
||||||
_, err = io.CopyBuffer(file, res.Body, buf)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithField("error", errors.WithStack(err)).Error("failed to copy archive file to disk")
|
zap.S().Errorw("failed to copy file to disk", zap.Error(err))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the file so it can be opened to verify the checksum.
|
// Close the file so it can be opened to verify the checksum.
|
||||||
if err := file.Close(); err != nil {
|
if err := file.Close(); err != nil {
|
||||||
l.WithField("error", errors.WithStack(err)).Error("failed to close archive file")
|
zap.S().Errorw("failed to close archive file", zap.Error(err))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
zap.S().Debug("server archive has been downloaded, computing checksum..", zap.String("server", serverID))
|
||||||
l.WithField("server", serverID).Debug("server archive downloaded, computing checksum...")
|
|
||||||
|
|
||||||
// Open the archive file for computing a checksum.
|
// Open the archive file for computing a checksum.
|
||||||
file, err = os.Open(archivePath)
|
file, err = os.Open(archivePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithField("error", errors.WithStack(err)).Error("failed to open archive on disk")
|
zap.S().Errorw("failed to open file on disk", zap.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute the sha256 checksum of the file.
|
// Compute the sha256 checksum of the file.
|
||||||
hash := sha256.New()
|
hash := sha256.New()
|
||||||
buf = make([]byte, 1024*4)
|
if _, err := io.Copy(hash, file); err != nil {
|
||||||
if _, err := io.CopyBuffer(hash, file, buf); err != nil {
|
zap.S().Errorw("failed to copy file for checksum verification", zap.Error(err))
|
||||||
l.WithField("error", errors.WithStack(err)).Error("failed to copy archive file for checksum verification")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the two checksums.
|
// Verify the two checksums.
|
||||||
if hex.EncodeToString(hash.Sum(nil)) != res.Header.Get("X-Checksum") {
|
if hex.EncodeToString(hash.Sum(nil)) != res.Header.Get("X-Checksum") {
|
||||||
l.Error("checksum verification failed for archive")
|
zap.S().Errorw("checksum failed verification")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the file.
|
// Close the file.
|
||||||
if err := file.Close(); err != nil {
|
if err := file.Close(); err != nil {
|
||||||
l.WithField("error", errors.WithStack(err)).Error("failed to close archive file after calculating checksum")
|
zap.S().Errorw("failed to close archive file", zap.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Info("server archive transfer was successful")
|
zap.S().Infow("server archive transfer was successful", zap.String("server", serverID))
|
||||||
|
|
||||||
// Get the server data from the request.
|
// Get the server data from the request.
|
||||||
serverData, t, _, _ := jsonparser.Get(data, "server")
|
serverData, t, _, _ := jsonparser.Get(data, "server")
|
||||||
if t != jsonparser.Object {
|
if t != jsonparser.Object {
|
||||||
l.Error("invalid server data passed in request")
|
zap.S().Errorw("invalid server data passed in request")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new server installer (note this does not execute the install script)
|
// Create a new server installer (note this does not execute the install script)
|
||||||
i, err := installer.New(serverData)
|
i, err := installer.New(serverData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithField("error", errors.WithStack(err)).Error("failed to validate received server data")
|
zap.S().Warnw("failed to validate the received server data", zap.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,7 +283,7 @@ func postTransfer(c *gin.Context) {
|
|||||||
|
|
||||||
// Un-archive the archive. That sounds weird..
|
// Un-archive the archive. That sounds weird..
|
||||||
if err := archiver.NewTarGz().Unarchive(archivePath, i.Server().Filesystem.Path()); err != nil {
|
if err := archiver.NewTarGz().Unarchive(archivePath, i.Server().Filesystem.Path()); err != nil {
|
||||||
l.WithField("error", errors.WithStack(err)).Error("failed to extract server archive")
|
zap.S().Errorw("failed to extract archive", zap.String("server", serverID), zap.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,16 +298,15 @@ func postTransfer(c *gin.Context) {
|
|||||||
rerr, err := api.NewRequester().SendTransferSuccess(serverID)
|
rerr, err := api.NewRequester().SendTransferSuccess(serverID)
|
||||||
if rerr != nil || err != nil {
|
if rerr != nil || err != nil {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithField("error", errors.WithStack(err)).Error("failed to notify panel of transfer success")
|
zap.S().Errorw("failed to notify panel with transfer success", zap.String("server", serverID), zap.Error(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
l.WithField("error", errors.WithStack(rerr)).Error("panel responded with error after transfer success")
|
zap.S().Errorw("panel returned an error when notifying of a transfer success", zap.String("server", serverID), zap.Error(errors.New(rerr.String())))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Info("successfully notified panel of transfer success")
|
zap.S().Debugw("successfully notified panel about transfer success", zap.String("server", serverID))
|
||||||
}(buf.Bytes())
|
}(buf.Bytes())
|
||||||
|
|
||||||
c.Status(http.StatusAccepted)
|
c.Status(http.StatusAccepted)
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
|
|
||||||
type BackupPayload struct {
|
type BackupPayload struct {
|
||||||
jwt.Payload
|
jwt.Payload
|
||||||
|
|
||||||
ServerUuid string `json:"server_uuid"`
|
ServerUuid string `json:"server_uuid"`
|
||||||
BackupUuid string `json:"backup_uuid"`
|
BackupUuid string `json:"backup_uuid"`
|
||||||
UniqueId string `json:"unique_id"`
|
UniqueId string `json:"unique_id"`
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
package tokens
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gbrlsnchs/jwt/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UploadPayload struct {
|
|
||||||
jwt.Payload
|
|
||||||
|
|
||||||
ServerUuid string `json:"server_uuid"`
|
|
||||||
UniqueId string `json:"unique_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the JWT payload.
|
|
||||||
func (p *UploadPayload) GetPayload() *jwt.Payload {
|
|
||||||
return &p.Payload
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determines if this JWT is valid for the given request cycle. If the
|
|
||||||
// unique ID passed in the token has already been seen before this will
|
|
||||||
// return false. This allows us to use this JWT as a one-time token that
|
|
||||||
// validates all of the request.
|
|
||||||
func (p *UploadPayload) IsUniqueRequest() bool {
|
|
||||||
return getTokenStore().IsValidToken(p.UniqueId)
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ package websocket
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/pterodactyl/wings/events"
|
|
||||||
"github.com/pterodactyl/wings/server"
|
"github.com/pterodactyl/wings/server"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -27,44 +26,43 @@ func (h *Handler) ListenForExpiration(ctx context.Context) {
|
|||||||
jwt := h.GetJwt()
|
jwt := h.GetJwt()
|
||||||
if jwt != nil {
|
if jwt != nil {
|
||||||
if jwt.ExpirationTime.Unix()-time.Now().Unix() <= 0 {
|
if jwt.ExpirationTime.Unix()-time.Now().Unix() <= 0 {
|
||||||
_ = h.SendJson(&Message{Event: TokenExpiredEvent})
|
h.SendJson(&Message{Event: TokenExpiredEvent})
|
||||||
} else if jwt.ExpirationTime.Unix()-time.Now().Unix() <= 60 {
|
} else if jwt.ExpirationTime.Unix()-time.Now().Unix() <= 180 {
|
||||||
_ = h.SendJson(&Message{Event: TokenExpiringEvent})
|
h.SendJson(&Message{Event: TokenExpiringEvent})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var e = []string{
|
|
||||||
server.StatsEvent,
|
|
||||||
server.StatusEvent,
|
|
||||||
server.ConsoleOutputEvent,
|
|
||||||
server.InstallOutputEvent,
|
|
||||||
server.InstallStartedEvent,
|
|
||||||
server.InstallCompletedEvent,
|
|
||||||
server.DaemonMessageEvent,
|
|
||||||
server.BackupCompletedEvent,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listens for different events happening on a server and sends them along
|
// Listens for different events happening on a server and sends them along
|
||||||
// to the connected websocket.
|
// to the connected websocket.
|
||||||
func (h *Handler) ListenForServerEvents(ctx context.Context) {
|
func (h *Handler) ListenForServerEvents(ctx context.Context) {
|
||||||
eventChannel := make(chan events.Event)
|
events := []string{
|
||||||
for _, event := range e {
|
server.StatsEvent,
|
||||||
|
server.StatusEvent,
|
||||||
|
server.ConsoleOutputEvent,
|
||||||
|
server.InstallOutputEvent,
|
||||||
|
server.DaemonMessageEvent,
|
||||||
|
server.BackupCompletedEvent,
|
||||||
|
}
|
||||||
|
|
||||||
|
eventChannel := make(chan server.Event)
|
||||||
|
for _, event := range events {
|
||||||
h.server.Events().Subscribe(event, eventChannel)
|
h.server.Events().Subscribe(event, eventChannel)
|
||||||
}
|
}
|
||||||
|
|
||||||
for d := range eventChannel {
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
for _, event := range e {
|
for _, event := range events {
|
||||||
h.server.Events().Unsubscribe(event, eventChannel)
|
h.server.Events().Unsubscribe(event, eventChannel)
|
||||||
}
|
}
|
||||||
|
|
||||||
close(eventChannel)
|
close(eventChannel)
|
||||||
default:
|
default:
|
||||||
_ = h.SendJson(&Message{
|
// Listen for different events emitted by the server and respond to them appropriately.
|
||||||
|
for d := range eventChannel {
|
||||||
|
h.SendJson(&Message{
|
||||||
Event: d.Topic,
|
Event: d.Topic,
|
||||||
Args: []string{d.Data},
|
Args: []string{d.Data},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ type Message struct {
|
|||||||
//
|
//
|
||||||
// - status : Returns the server's power state.
|
// - status : Returns the server's power state.
|
||||||
// - logs : Returns the server log data at the time of the request.
|
// - logs : Returns the server log data at the time of the request.
|
||||||
// - power : Performs a power action against the server based the data.
|
// - power : Performs a power action aganist the server based the data.
|
||||||
// - command : Performs a command on a server using the data field.
|
// - command : Performs a command on a server using the data field.
|
||||||
Event string `json:"event"`
|
Event string `json:"event"`
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package websocket
|
package websocket
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
@@ -10,10 +9,10 @@ import (
|
|||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
"github.com/pterodactyl/wings/environment"
|
|
||||||
"github.com/pterodactyl/wings/router/tokens"
|
"github.com/pterodactyl/wings/router/tokens"
|
||||||
"github.com/pterodactyl/wings/server"
|
"github.com/pterodactyl/wings/server"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -58,24 +57,7 @@ func GetHandler(s *server.Server, w http.ResponseWriter, r *http.Request) (*Hand
|
|||||||
// Ensure that the websocket request is originating from the Panel itself,
|
// Ensure that the websocket request is originating from the Panel itself,
|
||||||
// and not some other location.
|
// and not some other location.
|
||||||
CheckOrigin: func(r *http.Request) bool {
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
o := r.Header.Get("Origin")
|
return r.Header.Get("Origin") == config.Get().PanelLocation
|
||||||
if o == config.Get().PanelLocation {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, origin := range config.Get().AllowedOrigins {
|
|
||||||
if origin == "*" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if o != origin {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,7 +137,7 @@ func (h *Handler) TokenValid() error {
|
|||||||
// Sends an error back to the connected websocket instance by checking the permissions
|
// Sends an error back to the connected websocket instance by checking the permissions
|
||||||
// of the token. If the user has the "receive-errors" grant we will send back the actual
|
// of the token. If the user has the "receive-errors" grant we will send back the actual
|
||||||
// error message, otherwise we just send back a standard error message.
|
// error message, otherwise we just send back a standard error message.
|
||||||
func (h *Handler) SendErrorJson(msg Message, err error, shouldLog ...bool) error {
|
func (h *Handler) SendErrorJson(msg Message, err error) error {
|
||||||
j := h.GetJwt()
|
j := h.GetJwt()
|
||||||
|
|
||||||
message := "an unexpected error was encountered while handling this request"
|
message := "an unexpected error was encountered while handling this request"
|
||||||
@@ -168,12 +150,10 @@ func (h *Handler) SendErrorJson(msg Message, err error, shouldLog ...bool) error
|
|||||||
wsm := Message{Event: ErrorEvent}
|
wsm := Message{Event: ErrorEvent}
|
||||||
wsm.Args = []string{m}
|
wsm.Args = []string{m}
|
||||||
|
|
||||||
if len(shouldLog) == 0 || (len(shouldLog) == 1 && shouldLog[0] == true) {
|
|
||||||
if !server.IsSuspendedError(err) {
|
if !server.IsSuspendedError(err) {
|
||||||
h.server.Log().WithFields(log.Fields{"event": msg.Event, "error_identifier": u.String(), "error": err}).
|
h.server.Log().WithFields(log.Fields{"event": msg.Event, "error_identifier": u.String(), "error": err}).
|
||||||
Error("failed to handle websocket process; an error was encountered processing an event")
|
Error("failed to handle websocket process; an error was encountered processing an event")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return h.unsafeSendJson(wsm)
|
return h.unsafeSendJson(wsm)
|
||||||
}
|
}
|
||||||
@@ -264,8 +244,8 @@ func (h *Handler) HandleInbound(m Message) error {
|
|||||||
|
|
||||||
// Only send the current disk usage if the server is offline, if docker container is running,
|
// Only send the current disk usage if the server is offline, if docker container is running,
|
||||||
// Environment#EnableResourcePolling() will send this data to all clients.
|
// Environment#EnableResourcePolling() will send this data to all clients.
|
||||||
if state == environment.ProcessOfflineState {
|
if state == server.ProcessOfflineState {
|
||||||
_ = h.server.Filesystem.HasSpaceAvailable(false)
|
_ = h.server.Filesystem.HasSpaceAvailable()
|
||||||
|
|
||||||
b, _ := json.Marshal(h.server.Proc())
|
b, _ := json.Marshal(h.server.Proc())
|
||||||
h.SendJson(&Message{
|
h.SendJson(&Message{
|
||||||
@@ -278,34 +258,37 @@ func (h *Handler) HandleInbound(m Message) error {
|
|||||||
}
|
}
|
||||||
case SetStateEvent:
|
case SetStateEvent:
|
||||||
{
|
{
|
||||||
action := server.PowerAction(strings.Join(m.Args, ""))
|
switch strings.Join(m.Args, "") {
|
||||||
|
case "start":
|
||||||
actions := make(map[server.PowerAction]string)
|
if h.GetJwt().HasPermission(PermissionSendPowerStart) {
|
||||||
actions[server.PowerActionStart] = PermissionSendPowerStart
|
return h.server.Environment.Start()
|
||||||
actions[server.PowerActionStop] = PermissionSendPowerStop
|
|
||||||
actions[server.PowerActionRestart] = PermissionSendPowerRestart
|
|
||||||
actions[server.PowerActionTerminate] = PermissionSendPowerStop
|
|
||||||
|
|
||||||
// Check that they have permission to perform this action if it is needed.
|
|
||||||
if permission, exists := actions[action]; exists {
|
|
||||||
if !h.GetJwt().HasPermission(permission) {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
break
|
||||||
|
case "stop":
|
||||||
|
if h.GetJwt().HasPermission(PermissionSendPowerStop) {
|
||||||
|
return h.server.Environment.Stop()
|
||||||
}
|
}
|
||||||
|
break
|
||||||
err := h.server.HandlePowerAction(action)
|
case "restart":
|
||||||
if errors.Is(err, context.DeadlineExceeded) {
|
if h.GetJwt().HasPermission(PermissionSendPowerRestart) {
|
||||||
m, _ := h.GetErrorMessage("another power action is currently being processed for this server, please try again later")
|
// If the server is alreay restarting don't do anything. Perhaps we send back an event
|
||||||
|
// in the future for this? For now no reason to knowingly trigger an error by trying to
|
||||||
h.SendJson(&Message{
|
// restart a process already restarting.
|
||||||
Event: ErrorEvent,
|
if h.server.Environment.IsRestarting() {
|
||||||
Args: []string{m},
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return h.server.Environment.Restart()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "kill":
|
||||||
|
if h.GetJwt().HasPermission(PermissionSendPowerStop) {
|
||||||
|
return h.server.Environment.Terminate(os.Kill)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
case SendServerLogsEvent:
|
case SendServerLogsEvent:
|
||||||
{
|
{
|
||||||
@@ -333,7 +316,7 @@ func (h *Handler) HandleInbound(m Message) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.server.GetState() == environment.ProcessOfflineState {
|
if h.server.GetState() == server.ProcessOfflineState {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
17
server/allocations.go
Normal file
17
server/allocations.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
// Defines the allocations available for a given server. When using the Docker environment
|
||||||
|
// driver these correspond to mappings for the container that allow external connections.
|
||||||
|
type Allocations struct {
|
||||||
|
// Defines the default allocation that should be used for this server. This is
|
||||||
|
// what will be used for {SERVER_IP} and {SERVER_PORT} when modifying configuration
|
||||||
|
// files or the startup arguments for a server.
|
||||||
|
DefaultMapping struct {
|
||||||
|
Ip string `json:"ip"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
} `json:"default"`
|
||||||
|
|
||||||
|
// Mappings contains all of the ports that should be assigned to a given server
|
||||||
|
// attached to the IP they correspond to.
|
||||||
|
Mappings map[string][]int `json:"mappings"`
|
||||||
|
}
|
||||||
@@ -101,9 +101,7 @@ func (a *Archiver) Checksum() (string, error) {
|
|||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
hash := sha256.New()
|
hash := sha256.New()
|
||||||
|
if _, err := io.Copy(hash, file); err != nil {
|
||||||
buf := make([]byte, 1024*4)
|
|
||||||
if _, err := io.CopyBuffer(hash, file, buf); err != nil {
|
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,13 @@ func (s *Server) notifyPanelOfBackup(uuid string, ad *backup.ArchiveDetails, suc
|
|||||||
s.Log().WithFields(log.Fields{
|
s.Log().WithFields(log.Fields{
|
||||||
"backup": uuid,
|
"backup": uuid,
|
||||||
"error": err,
|
"error": err,
|
||||||
}).Error("failed to notify panel of backup status due to wings error")
|
}).Error("failed to notify panel of backup status due to internal code error")
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.Log().WithField("backup", uuid).Warn(rerr.String())
|
||||||
|
|
||||||
return errors.New(rerr.String())
|
return errors.New(rerr.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,19 +90,11 @@ func (s *Server) Backup(b backup.BackupInterface) error {
|
|||||||
if notifyError := s.notifyPanelOfBackup(b.Identifier(), &backup.ArchiveDetails{}, false); notifyError != nil {
|
if notifyError := s.notifyPanelOfBackup(b.Identifier(), &backup.ArchiveDetails{}, false); notifyError != nil {
|
||||||
s.Log().WithFields(log.Fields{
|
s.Log().WithFields(log.Fields{
|
||||||
"backup": b.Identifier(),
|
"backup": b.Identifier(),
|
||||||
"error": notifyError,
|
"error": err,
|
||||||
}).Warn("failed to notify panel of failed backup state")
|
}).Warn("failed to notify panel of failed backup state")
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Events().PublishJson(BackupCompletedEvent+":"+b.Identifier(), map[string]interface{}{
|
return errors.WithStack(err)
|
||||||
"uuid": b.Identifier(),
|
|
||||||
"is_successful": false,
|
|
||||||
"checksum": "",
|
|
||||||
"checksum_type": "sha1",
|
|
||||||
"file_size": 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
return errors.Wrap(err, "error while generating server backup")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to notify the panel about the status of this backup. If for some reason this request
|
// Try to notify the panel about the status of this backup. If for some reason this request
|
||||||
@@ -115,9 +109,7 @@ func (s *Server) Backup(b backup.BackupInterface) error {
|
|||||||
// the frontend for the server.
|
// the frontend for the server.
|
||||||
s.Events().PublishJson(BackupCompletedEvent+":"+b.Identifier(), map[string]interface{}{
|
s.Events().PublishJson(BackupCompletedEvent+":"+b.Identifier(), map[string]interface{}{
|
||||||
"uuid": b.Identifier(),
|
"uuid": b.Identifier(),
|
||||||
"is_successful": true,
|
"sha256_hash": ad.Checksum,
|
||||||
"checksum": ad.Checksum,
|
|
||||||
"checksum_type": "sha1",
|
|
||||||
"file_size": ad.Size,
|
"file_size": ad.Size,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
gzip "github.com/klauspost/pgzip"
|
gzip "github.com/klauspost/pgzip"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/remeh/sizedwaitgroup"
|
"github.com/remeh/sizedwaitgroup"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
@@ -26,23 +24,14 @@ type Archive struct {
|
|||||||
func (a *Archive) Create(dst string, ctx context.Context) (os.FileInfo, error) {
|
func (a *Archive) Create(dst string, ctx context.Context) (os.FileInfo, error) {
|
||||||
f, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
f, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
maxCpu := runtime.NumCPU() / 2
|
gzw := gzip.NewWriter(f)
|
||||||
if maxCpu > 4 {
|
|
||||||
maxCpu = 4
|
|
||||||
}
|
|
||||||
|
|
||||||
gzw, _ := gzip.NewWriterLevel(f, gzip.BestSpeed)
|
|
||||||
_ = gzw.SetConcurrency(1<<20, maxCpu)
|
|
||||||
|
|
||||||
defer gzw.Flush()
|
|
||||||
defer gzw.Close()
|
defer gzw.Close()
|
||||||
|
|
||||||
tw := tar.NewWriter(gzw)
|
tw := tar.NewWriter(gzw)
|
||||||
defer tw.Flush()
|
|
||||||
defer tw.Close()
|
defer tw.Close()
|
||||||
|
|
||||||
wg := sizedwaitgroup.New(10)
|
wg := sizedwaitgroup.New(10)
|
||||||
@@ -50,17 +39,23 @@ func (a *Archive) Create(dst string, ctx context.Context) (os.FileInfo, error) {
|
|||||||
// Iterate over all of the files to be included and put them into the archive. This is
|
// Iterate over all of the files to be included and put them into the archive. This is
|
||||||
// done as a concurrent goroutine to speed things along. If an error is encountered at
|
// done as a concurrent goroutine to speed things along. If an error is encountered at
|
||||||
// any step, the entire process is aborted.
|
// any step, the entire process is aborted.
|
||||||
for _, p := range a.Files.All() {
|
for p, s := range a.Files.All() {
|
||||||
p := p
|
if (*s).IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pa := p
|
||||||
|
st := s
|
||||||
|
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
wg.Add()
|
wg.Add()
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return errors.WithStack(ctx.Err())
|
return ctx.Err()
|
||||||
default:
|
default:
|
||||||
return a.addToArchive(p, tw)
|
return a.addToArchive(pa, st, tw)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -75,48 +70,33 @@ func (a *Archive) Create(dst string, ctx context.Context) (os.FileInfo, error) {
|
|||||||
log.WithField("location", dst).Warn("failed to delete corrupted backup archive")
|
log.WithField("location", dst).Warn("failed to delete corrupted backup archive")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
st, err := f.Stat()
|
st, err := f.Stat()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return st, nil
|
return st, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds a single file to the existing tar archive writer.
|
// Adds a single file to the existing tar archive writer.
|
||||||
func (a *Archive) addToArchive(p string, w *tar.Writer) error {
|
func (a *Archive) addToArchive(p string, s *os.FileInfo, w *tar.Writer) error {
|
||||||
f, err := os.Open(p)
|
f, err := os.Open(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If you try to backup something that no longer exists (got deleted somewhere during the process
|
return err
|
||||||
// but not by this process), just skip over it and don't kill the entire backup.
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
s, err := f.Stat()
|
st := *s
|
||||||
if err != nil {
|
|
||||||
// Same as above, don't kill the process just because the file no longer exists.
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
header := &tar.Header{
|
header := &tar.Header{
|
||||||
// Trim the long server path from the name of the file so that the resulting
|
// Trim the long server path from the name of the file so that the resulting
|
||||||
// archive is exactly how the user would see it in the panel file manager.
|
// archive is exactly how the user would see it in the panel file manager.
|
||||||
Name: strings.TrimPrefix(p, a.TrimPrefix),
|
Name: strings.TrimPrefix(p, a.TrimPrefix),
|
||||||
Size: s.Size(),
|
Size: st.Size(),
|
||||||
Mode: int64(s.Mode()),
|
Mode: int64(st.Mode()),
|
||||||
ModTime: s.ModTime(),
|
ModTime: st.ModTime(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// These actions must occur sequentially, even if this function is called multiple
|
// These actions must occur sequentially, even if this function is called multiple
|
||||||
@@ -125,12 +105,11 @@ func (a *Archive) addToArchive(p string, w *tar.Writer) error {
|
|||||||
defer a.Unlock()
|
defer a.Unlock()
|
||||||
|
|
||||||
if err := w.WriteHeader(header); err != nil {
|
if err := w.WriteHeader(header); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := make([]byte, 4*1024)
|
if _, err := io.Copy(w, f); err != nil {
|
||||||
if _, err := io.CopyBuffer(w, f, buf); err != nil {
|
return err
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package backup
|
package backup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha1"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -20,7 +20,6 @@ const (
|
|||||||
|
|
||||||
type ArchiveDetails struct {
|
type ArchiveDetails struct {
|
||||||
Checksum string `json:"checksum"`
|
Checksum string `json:"checksum"`
|
||||||
ChecksumType string `json:"checksum_type"`
|
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +27,6 @@ type ArchiveDetails struct {
|
|||||||
func (ad *ArchiveDetails) ToRequest(successful bool) api.BackupRequest {
|
func (ad *ArchiveDetails) ToRequest(successful bool) api.BackupRequest {
|
||||||
return api.BackupRequest{
|
return api.BackupRequest{
|
||||||
Checksum: ad.Checksum,
|
Checksum: ad.Checksum,
|
||||||
ChecksumType: ad.ChecksumType,
|
|
||||||
Size: ad.Size,
|
Size: ad.Size,
|
||||||
Successful: successful,
|
Successful: successful,
|
||||||
}
|
}
|
||||||
@@ -95,17 +93,16 @@ func (b *Backup) Size() (int64, error) {
|
|||||||
|
|
||||||
// Returns the SHA256 checksum of a backup.
|
// Returns the SHA256 checksum of a backup.
|
||||||
func (b *Backup) Checksum() ([]byte, error) {
|
func (b *Backup) Checksum() ([]byte, error) {
|
||||||
h := sha1.New()
|
h := sha256.New()
|
||||||
|
|
||||||
f, err := os.Open(b.Path())
|
f, err := os.Open(b.Path())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return []byte{}, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
buf := make([]byte, 1024*4)
|
if _, err := io.Copy(h, f); err != nil {
|
||||||
if _, err := io.CopyBuffer(h, f, buf); err != nil {
|
return []byte{}, errors.WithStack(err)
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return h.Sum(nil), nil
|
return h.Sum(nil), nil
|
||||||
@@ -148,7 +145,6 @@ func (b *Backup) Details() *ArchiveDetails {
|
|||||||
|
|
||||||
return &ArchiveDetails{
|
return &ArchiveDetails{
|
||||||
Checksum: checksum,
|
Checksum: checksum,
|
||||||
ChecksumType: "sha1",
|
|
||||||
Size: sz,
|
Size: sz,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,11 +24,11 @@ func LocateLocal(uuid string) (*LocalBackup, os.FileInfo, error) {
|
|||||||
|
|
||||||
st, err := os.Stat(b.Path())
|
st, err := os.Stat(b.Path())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, errors.WithStack(err)
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if st.IsDir() {
|
if st.IsDir() {
|
||||||
return nil, nil, errors.New("invalid archive, is directory")
|
return nil, nil, errors.New("invalid archive found; is directory")
|
||||||
}
|
}
|
||||||
|
|
||||||
return b, st, nil
|
return b, st, nil
|
||||||
@@ -48,7 +48,7 @@ func (b *LocalBackup) Generate(included *IncludedFiles, prefix string) (*Archive
|
|||||||
}
|
}
|
||||||
|
|
||||||
if _, err := a.Create(b.Path(), context.Background()); err != nil {
|
if _, err := a.Create(b.Path(), context.Background()); err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.Details(), nil
|
return b.Details(), nil
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -34,17 +33,17 @@ func (s *S3Backup) Generate(included *IncludedFiles, prefix string) (*ArchiveDet
|
|||||||
}
|
}
|
||||||
|
|
||||||
if _, err := a.Create(s.Path(), context.Background()); err != nil {
|
if _, err := a.Create(s.Path(), context.Background()); err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rc, err := os.Open(s.Path())
|
rc, err := os.Open(s.Path())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rc.Close()
|
defer rc.Close()
|
||||||
|
|
||||||
if resp, err := s.generateRemoteRequest(rc); err != nil {
|
if resp, err := s.generateRemoteRequest(rc); err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,29 @@
|
|||||||
package backup
|
package backup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
type IncludedFiles struct {
|
type IncludedFiles struct {
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
files []string
|
files map[string]*os.FileInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pushes an additional file or folder onto the struct.
|
// Pushes an additional file or folder onto the struct.
|
||||||
func (i *IncludedFiles) Push(p string) {
|
func (i *IncludedFiles) Push(info *os.FileInfo, p string) {
|
||||||
i.Lock()
|
i.Lock()
|
||||||
i.files = append(i.files, p) // ~~
|
defer i.Unlock()
|
||||||
i.Unlock()
|
|
||||||
|
if i.files == nil {
|
||||||
|
i.files = make(map[string]*os.FileInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
i.files[p] = info
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns all of the files that were marked as being included.
|
// Returns all of the files that were marked as being included.
|
||||||
func (i *IncludedFiles) All() []string {
|
func (i *IncludedFiles) All() map[string]*os.FileInfo {
|
||||||
i.RLock()
|
i.RLock()
|
||||||
defer i.RUnlock()
|
defer i.RUnlock()
|
||||||
|
|
||||||
|
|||||||
72
server/build_settings.go
Normal file
72
server/build_settings.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import "math"
|
||||||
|
|
||||||
|
// The build settings for a given server that impact docker container creation and
|
||||||
|
// resource limits for a server instance.
|
||||||
|
type BuildSettings struct {
|
||||||
|
// The total amount of memory in megabytes that this server is allowed to
|
||||||
|
// use on the host system.
|
||||||
|
MemoryLimit int64 `json:"memory_limit"`
|
||||||
|
|
||||||
|
// The amount of additional swap space to be provided to a container instance.
|
||||||
|
Swap int64 `json:"swap"`
|
||||||
|
|
||||||
|
// The relative weight for IO operations in a container. This is relative to other
|
||||||
|
// containers on the system and should be a value between 10 and 1000.
|
||||||
|
IoWeight uint16 `json:"io_weight"`
|
||||||
|
|
||||||
|
// The percentage of CPU that this instance is allowed to consume relative to
|
||||||
|
// the host. A value of 200% represents complete utilization of two cores. This
|
||||||
|
// should be a value between 1 and THREAD_COUNT * 100.
|
||||||
|
CpuLimit int64 `json:"cpu_limit"`
|
||||||
|
|
||||||
|
// The amount of disk space in megabytes that a server is allowed to use.
|
||||||
|
DiskSpace int64 `json:"disk_space"`
|
||||||
|
|
||||||
|
// Sets which CPU threads can be used by the docker instance.
|
||||||
|
Threads string `json:"threads"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Build() *BuildSettings {
|
||||||
|
return &s.Config().Build
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts the CPU limit for a server build into a number that can be better understood
|
||||||
|
// by the Docker environment. If there is no limit set, return -1 which will indicate to
|
||||||
|
// Docker that it has unlimited CPU quota.
|
||||||
|
func (b *BuildSettings) ConvertedCpuLimit() int64 {
|
||||||
|
if b.CpuLimit == 0 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.CpuLimit * 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the hard limit for memory usage to be 5% more than the amount of memory assigned to
|
||||||
|
// the server. If the memory limit for the server is < 4G, use 10%, if less than 2G use
|
||||||
|
// 15%. This avoids unexpected crashes from processes like Java which run over the limit.
|
||||||
|
func (b *BuildSettings) MemoryOverheadMultiplier() float64 {
|
||||||
|
if b.MemoryLimit <= 2048 {
|
||||||
|
return 1.15
|
||||||
|
} else if b.MemoryLimit <= 4096 {
|
||||||
|
return 1.10
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1.05
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BuildSettings) BoundedMemoryLimit() int64 {
|
||||||
|
return int64(math.Round(float64(b.MemoryLimit) * b.MemoryOverheadMultiplier() * 1_000_000))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the amount of swap available as a total in bytes. This is returned as the amount
|
||||||
|
// of memory available to the server initially, PLUS the amount of additional swap to include
|
||||||
|
// which is the format used by Docker.
|
||||||
|
func (b *BuildSettings) ConvertedSwap() int64 {
|
||||||
|
if b.Swap < 0 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
return (b.Swap * 1_000_000) + b.BoundedMemoryLimit()
|
||||||
|
}
|
||||||
@@ -1,32 +1,34 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gammazero/workerpool"
|
"github.com/pterodactyl/wings/parser"
|
||||||
"runtime"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Parent function that will update all of the defined configuration files for a server
|
// Parent function that will update all of the defined configuration files for a server
|
||||||
// automatically to ensure that they always use the specified values.
|
// automatically to ensure that they always use the specified values.
|
||||||
func (s *Server) UpdateConfigurationFiles() {
|
func (s *Server) UpdateConfigurationFiles() {
|
||||||
pool := workerpool.New(runtime.NumCPU())
|
wg := new(sync.WaitGroup)
|
||||||
|
|
||||||
files := s.ProcessConfiguration().ConfigurationFiles
|
files := s.ProcessConfiguration().ConfigurationFiles
|
||||||
for _, cf := range files {
|
for _, v := range files {
|
||||||
f := cf
|
wg.Add(1)
|
||||||
|
|
||||||
pool.Submit(func() {
|
go func(f parser.ConfigurationFile, server *Server) {
|
||||||
p, err := s.Filesystem.SafePath(f.FileName)
|
defer wg.Done()
|
||||||
|
|
||||||
|
p, err := server.Filesystem.SafePath(f.FileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log().WithField("error", err).Error("failed to generate safe path for configuration file")
|
server.Log().WithField("error", err).Error("failed to generate safe path for configuration file")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := f.Parse(p, false); err != nil {
|
if err := f.Parse(p, false); err != nil {
|
||||||
s.Log().WithField("error", err).Error("failed to parse and update server configuration file")
|
server.Log().WithField("error", err).Error("failed to parse and update server configuration file")
|
||||||
}
|
}
|
||||||
})
|
}(v, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
pool.StopWait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,41 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/pterodactyl/wings/environment"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type EnvironmentVariables map[string]interface{}
|
||||||
|
|
||||||
|
// Ugly hacky function to handle environment variables that get passed through as not-a-string
|
||||||
|
// from the Panel. Ideally we'd just say only pass strings, but that is a fragile idea and if a
|
||||||
|
// string wasn't passed through you'd cause a crash or the server to become unavailable. For now
|
||||||
|
// try to handle the most likely values from the JSON and hope for the best.
|
||||||
|
func (ev EnvironmentVariables) Get(key string) string {
|
||||||
|
val, ok := ev[key]
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
switch val.(type) {
|
||||||
|
case int:
|
||||||
|
return strconv.Itoa(val.(int))
|
||||||
|
case int32:
|
||||||
|
return strconv.FormatInt(val.(int64), 10)
|
||||||
|
case int64:
|
||||||
|
return strconv.FormatInt(val.(int64), 10)
|
||||||
|
case float32:
|
||||||
|
return fmt.Sprintf("%f", val.(float32))
|
||||||
|
case float64:
|
||||||
|
return fmt.Sprintf("%f", val.(float64))
|
||||||
|
case bool:
|
||||||
|
return strconv.FormatBool(val.(bool))
|
||||||
|
}
|
||||||
|
|
||||||
|
return val.(string)
|
||||||
|
}
|
||||||
|
|
||||||
type Configuration struct {
|
type Configuration struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
|
|
||||||
@@ -20,16 +51,12 @@ type Configuration struct {
|
|||||||
// The command that should be used when booting up the server instance.
|
// The command that should be used when booting up the server instance.
|
||||||
Invocation string `json:"invocation"`
|
Invocation string `json:"invocation"`
|
||||||
|
|
||||||
// By default this is false, however if selected within the Panel while installing or re-installing a
|
|
||||||
// server, specific installation scripts will be skipped for the server process.
|
|
||||||
SkipEggScripts bool `default:"false" json:"skip_egg_scripts"`
|
|
||||||
|
|
||||||
// An array of environment variables that should be passed along to the running
|
// An array of environment variables that should be passed along to the running
|
||||||
// server process.
|
// server process.
|
||||||
EnvVars environment.Variables `json:"environment"`
|
EnvVars EnvironmentVariables `json:"environment"`
|
||||||
|
|
||||||
Allocations environment.Allocations `json:"allocations"`
|
Allocations Allocations `json:"allocations"`
|
||||||
Build environment.Limits `json:"build"`
|
Build BuildSettings `json:"build"`
|
||||||
CrashDetectionEnabled bool `default:"true" json:"enabled" yaml:"enabled"`
|
CrashDetectionEnabled bool `default:"true" json:"enabled" yaml:"enabled"`
|
||||||
Mounts []Mount `json:"mounts"`
|
Mounts []Mount `json:"mounts"`
|
||||||
Resources ResourceUsage `json:"resources"`
|
Resources ResourceUsage `json:"resources"`
|
||||||
@@ -37,6 +64,9 @@ type Configuration struct {
|
|||||||
Container struct {
|
Container struct {
|
||||||
// Defines the Docker image that will be used for this server
|
// Defines the Docker image that will be used for this server
|
||||||
Image string `json:"image,omitempty"`
|
Image string `json:"image,omitempty"`
|
||||||
|
// If set to true, OOM killer will be disabled on the server's Docker container.
|
||||||
|
// If not present (nil) we will default to disabling it.
|
||||||
|
OomDisabled bool `default:"true" json:"oom_disabled"`
|
||||||
} `json:"container,omitempty"`
|
} `json:"container,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,20 +77,6 @@ func (s *Server) Config() *Configuration {
|
|||||||
return &s.cfg
|
return &s.cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) DiskSpace() int64 {
|
|
||||||
s.cfg.mu.RLock()
|
|
||||||
defer s.cfg.mu.RUnlock()
|
|
||||||
|
|
||||||
return s.cfg.Build.DiskSpace
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) MemoryLimit() int64 {
|
|
||||||
s.cfg.mu.RLock()
|
|
||||||
defer s.cfg.mu.RUnlock()
|
|
||||||
|
|
||||||
return s.cfg.Build.MemoryLimit
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Configuration) GetUuid() string {
|
func (c *Configuration) GetUuid() string {
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
defer c.mu.RUnlock()
|
defer c.mu.RUnlock()
|
||||||
|
|||||||
@@ -3,97 +3,25 @@ package server
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/mitchellh/colorstring"
|
"github.com/mitchellh/colorstring"
|
||||||
"github.com/pterodactyl/wings/config"
|
"io"
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConsoleThrottler struct {
|
type Console struct {
|
||||||
sync.RWMutex
|
Server *Server
|
||||||
config.ConsoleThrottles
|
HandlerFunc *func(string)
|
||||||
|
|
||||||
// The total number of activations that have occurred thus far.
|
|
||||||
activations uint64
|
|
||||||
|
|
||||||
// The total number of lines processed so far during the given time period.
|
|
||||||
lines uint64
|
|
||||||
|
|
||||||
lastIntervalTime *time.Time
|
|
||||||
lastDecayTime *time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increments the number of activations for a server.
|
var _ io.Writer = Console{}
|
||||||
func (ct *ConsoleThrottler) AddActivation() uint64 {
|
|
||||||
ct.Lock()
|
|
||||||
defer ct.Unlock()
|
|
||||||
|
|
||||||
ct.activations += 1
|
func (c Console) Write(b []byte) (int, error) {
|
||||||
|
if c.HandlerFunc != nil {
|
||||||
|
l := make([]byte, len(b))
|
||||||
|
copy(l, b)
|
||||||
|
|
||||||
return ct.activations
|
(*c.HandlerFunc)(string(l))
|
||||||
}
|
|
||||||
|
|
||||||
// Decrements the number of activations for a server.
|
|
||||||
func (ct *ConsoleThrottler) RemoveActivation() uint64 {
|
|
||||||
ct.Lock()
|
|
||||||
defer ct.Unlock()
|
|
||||||
|
|
||||||
if ct.activations == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ct.activations -= 1
|
return len(b), nil
|
||||||
|
|
||||||
return ct.activations
|
|
||||||
}
|
|
||||||
|
|
||||||
// Increment the total count of lines that we have processed so far.
|
|
||||||
func (ct *ConsoleThrottler) IncrementLineCount() uint64 {
|
|
||||||
return atomic.AddUint64(&ct.lines, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset the line count to zero.
|
|
||||||
func (ct *ConsoleThrottler) ResetLineCount() {
|
|
||||||
atomic.SwapUint64(&ct.lines, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handles output from a server's console. This code ensures that a server is not outputting
|
|
||||||
// an excessive amount of data to the console that could indicate a malicious or run-away process
|
|
||||||
// and lead to performance issues for other users.
|
|
||||||
//
|
|
||||||
// This was much more of a problem for the NodeJS version of the daemon which struggled to handle
|
|
||||||
// large volumes of output. However, this code is much more performant so I generally feel a lot
|
|
||||||
// better about it's abilities.
|
|
||||||
//
|
|
||||||
// However, extreme output is still somewhat of a DoS attack vector against this software since we
|
|
||||||
// are still logging it to the disk temporarily and will want to avoid dumping a huge amount of
|
|
||||||
// data all at once. These values are all configurable via the wings configuration file, however the
|
|
||||||
// defaults have been in the wild for almost two years at the time of this writing, so I feel quite
|
|
||||||
// confident in them.
|
|
||||||
func (ct *ConsoleThrottler) Handle() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the throttler instance for the server or creates a new one.
|
|
||||||
func (s *Server) Throttler() *ConsoleThrottler {
|
|
||||||
s.throttleLock.RLock()
|
|
||||||
|
|
||||||
if s.throttler == nil {
|
|
||||||
// Release the read lock so that we can acquire a normal lock on the process and
|
|
||||||
// make modifications to the throttler.
|
|
||||||
s.throttleLock.RUnlock()
|
|
||||||
|
|
||||||
s.throttleLock.Lock()
|
|
||||||
s.throttler = &ConsoleThrottler{
|
|
||||||
ConsoleThrottles: config.Get().Throttles,
|
|
||||||
}
|
|
||||||
s.throttleLock.Unlock()
|
|
||||||
|
|
||||||
return s.throttler
|
|
||||||
} else {
|
|
||||||
defer s.throttleLock.RUnlock()
|
|
||||||
return s.throttler
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sends output to the server console formatted to appear correctly as being sent
|
// Sends output to the server console formatted to appear correctly as being sent
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
"github.com/pterodactyl/wings/environment"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -35,7 +34,7 @@ func (cd *CrashHandler) SetLastCrash(t time.Time) {
|
|||||||
// if it was the result of an event that we should try to recover from.
|
// if it was the result of an event that we should try to recover from.
|
||||||
//
|
//
|
||||||
// This function assumes it is called under circumstances where a crash is suspected
|
// This function assumes it is called under circumstances where a crash is suspected
|
||||||
// of occurring. It will not do anything to determine if it was actually a crash, just
|
// of occuring. It will not do anything to determine if it was actually a crash, just
|
||||||
// look at the exit state and check if it meets the criteria of being called a crash
|
// look at the exit state and check if it meets the criteria of being called a crash
|
||||||
// by Wings.
|
// by Wings.
|
||||||
//
|
//
|
||||||
@@ -45,7 +44,7 @@ func (s *Server) handleServerCrash() error {
|
|||||||
// No point in doing anything here if the server isn't currently offline, there
|
// No point in doing anything here if the server isn't currently offline, there
|
||||||
// is no reason to do a crash detection event. If the server crash detection is
|
// is no reason to do a crash detection event. If the server crash detection is
|
||||||
// disabled we want to skip anything after this as well.
|
// disabled we want to skip anything after this as well.
|
||||||
if s.GetState() != environment.ProcessOfflineState || !s.Config().CrashDetectionEnabled {
|
if s.GetState() != ProcessOfflineState || !s.Config().CrashDetectionEnabled {
|
||||||
if !s.Config().CrashDetectionEnabled {
|
if !s.Config().CrashDetectionEnabled {
|
||||||
s.Log().Debug("server triggered crash detection but handler is disabled for server process")
|
s.Log().Debug("server triggered crash detection but handler is disabled for server process")
|
||||||
|
|
||||||
@@ -75,7 +74,7 @@ func (s *Server) handleServerCrash() error {
|
|||||||
c := s.crasher.LastCrashTime()
|
c := s.crasher.LastCrashTime()
|
||||||
// If the last crash time was within the last 60 seconds we do not want to perform
|
// If the last crash time was within the last 60 seconds we do not want to perform
|
||||||
// an automatic reboot of the process. Return an error that can be handled.
|
// an automatic reboot of the process. Return an error that can be handled.
|
||||||
if !c.IsZero() && c.Add(time.Second*60).After(time.Now()) {
|
if !c.IsZero() && c.Add(time.Second * 60).After(time.Now()) {
|
||||||
s.PublishConsoleOutputFromDaemon("Aborting automatic reboot: last crash occurred less than 60 seconds ago.")
|
s.PublishConsoleOutputFromDaemon("Aborting automatic reboot: last crash occurred less than 60 seconds ago.")
|
||||||
|
|
||||||
return &crashTooFrequent{}
|
return &crashTooFrequent{}
|
||||||
@@ -83,5 +82,5 @@ func (s *Server) handleServerCrash() error {
|
|||||||
|
|
||||||
s.crasher.SetLastCrash(time.Now())
|
s.crasher.SetLastCrash(time.Now())
|
||||||
|
|
||||||
return s.HandlePowerAction(PowerActionStart)
|
return s.Environment.Start()
|
||||||
}
|
}
|
||||||
@@ -1,42 +1,15 @@
|
|||||||
package environment
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/pterodactyl/wings/events"
|
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
ConsoleOutputEvent = "console output"
|
|
||||||
StateChangeEvent = "state change"
|
|
||||||
ResourceEvent = "resources"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
ProcessOfflineState = "offline"
|
|
||||||
ProcessStartingState = "starting"
|
|
||||||
ProcessRunningState = "running"
|
|
||||||
ProcessStoppingState = "stopping"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Defines the basic interface that all environments need to implement so that
|
// Defines the basic interface that all environments need to implement so that
|
||||||
// a server can be properly controlled.
|
// a server can be properly controlled.
|
||||||
type ProcessEnvironment interface {
|
type Environment interface {
|
||||||
// Returns the name of the environment.
|
// Returns the name of the environment.
|
||||||
Type() string
|
Type() string
|
||||||
|
|
||||||
// Returns the environment configuration to the caller.
|
|
||||||
Config() *Configuration
|
|
||||||
|
|
||||||
// Returns an event emitter instance that can be hooked into to listen for different
|
|
||||||
// events that are fired by the environment. This should not allow someone to publish
|
|
||||||
// events, only subscribe to them.
|
|
||||||
Events() *events.EventBus
|
|
||||||
|
|
||||||
// Determines if the server instance exists. For example, in a docker environment
|
|
||||||
// this should confirm that the container is created and in a bootable state. In
|
|
||||||
// a basic CLI environment this can probably just return true right away.
|
|
||||||
Exists() (bool, error)
|
|
||||||
|
|
||||||
// Determines if the environment is currently active and running a server process
|
// Determines if the environment is currently active and running a server process
|
||||||
// for this specific server instance.
|
// for this specific server instance.
|
||||||
IsRunning() (bool, error)
|
IsRunning() (bool, error)
|
||||||
@@ -58,10 +31,22 @@ type ProcessEnvironment interface {
|
|||||||
// not be returned.
|
// not be returned.
|
||||||
Stop() error
|
Stop() error
|
||||||
|
|
||||||
|
// Restart a server instance. If already stopped the process will be started. This function
|
||||||
|
// will return an error if the server is already performing a restart process as to avoid
|
||||||
|
// unnecessary double/triple/quad looping issues if multiple people press restart or spam the
|
||||||
|
// button to restart.
|
||||||
|
Restart() error
|
||||||
|
IsRestarting() bool
|
||||||
|
|
||||||
// Waits for a server instance to stop gracefully. If the server is still detected
|
// Waits for a server instance to stop gracefully. If the server is still detected
|
||||||
// as running after seconds, an error will be returned, or the server will be terminated
|
// as running after seconds, an error will be returned, or the server will be terminated
|
||||||
// depending on the value of the second argument.
|
// depending on the value of the second argument.
|
||||||
WaitForStop(seconds uint, terminate bool) error
|
WaitForStop(seconds int, terminate bool) error
|
||||||
|
|
||||||
|
// Determines if the server instance exists. For example, in a docker environment
|
||||||
|
// this should confirm that the container is created and in a bootable state. In
|
||||||
|
// a basic CLI environment this can probably just return true right away.
|
||||||
|
Exists() (bool, error)
|
||||||
|
|
||||||
// Terminates a running server instance using the provided signal. If the server
|
// Terminates a running server instance using the provided signal. If the server
|
||||||
// is not running no error should be returned.
|
// is not running no error should be returned.
|
||||||
@@ -85,10 +70,22 @@ type ProcessEnvironment interface {
|
|||||||
// send data into the environment's stdin.
|
// send data into the environment's stdin.
|
||||||
Attach() error
|
Attach() error
|
||||||
|
|
||||||
|
// Follows the output from the server console and will begin piping the output to
|
||||||
|
// the server's emitter.
|
||||||
|
FollowConsoleOutput() error
|
||||||
|
|
||||||
// Sends the provided command to the running server instance.
|
// Sends the provided command to the running server instance.
|
||||||
SendCommand(string) error
|
SendCommand(string) error
|
||||||
|
|
||||||
// Reads the log file for the process from the end backwards until the provided
|
// Reads the log file for the process from the end backwards until the provided
|
||||||
// number of lines is met.
|
// number of bytes is met.
|
||||||
Readlog(int) ([]string, error)
|
Readlog(int64) ([]string, error)
|
||||||
|
|
||||||
|
// Polls the given environment for resource usage of the server when the process
|
||||||
|
// is running.
|
||||||
|
EnableResourcePolling() error
|
||||||
|
|
||||||
|
// Disables the polling operation for resource usage and sets the required values
|
||||||
|
// to 0 in the server resource usage struct.
|
||||||
|
DisableResourcePolling() error
|
||||||
}
|
}
|
||||||
988
server/environment_docker.go
Normal file
988
server/environment_docker.go
Normal file
@@ -0,0 +1,988 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/apex/log"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/mount"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/docker/docker/daemon/logger/jsonfilelog"
|
||||||
|
"github.com/docker/go-connections/nat"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/pterodactyl/wings/api"
|
||||||
|
"github.com/pterodactyl/wings/config"
|
||||||
|
"golang.org/x/sync/semaphore"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Defines the base environment for Docker instances running through Wings.
|
||||||
|
type DockerEnvironment struct {
|
||||||
|
sync.RWMutex
|
||||||
|
|
||||||
|
Server *Server
|
||||||
|
|
||||||
|
// The Docker client being used for this instance.
|
||||||
|
Client *client.Client
|
||||||
|
|
||||||
|
// Tracks if we are currently attached to the server container. This allows us to attach
|
||||||
|
// once and then just use that attachment to stream logs out of the server and also stream
|
||||||
|
// commands back into it without constantly attaching and detaching.
|
||||||
|
attached bool
|
||||||
|
|
||||||
|
// Controls the hijacked response stream which exists only when we're attached to
|
||||||
|
// the running container instance.
|
||||||
|
stream types.HijackedResponse
|
||||||
|
|
||||||
|
// Holds the stats stream used by the polling commands so that we can easily close
|
||||||
|
// it out.
|
||||||
|
stats io.ReadCloser
|
||||||
|
|
||||||
|
// Locks when we're performing a restart to avoid trying to restart a process that is already
|
||||||
|
// being restarted.
|
||||||
|
restartSem *semaphore.Weighted
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set if this process is currently attached to the process.
|
||||||
|
func (d *DockerEnvironment) SetAttached(a bool) {
|
||||||
|
d.Lock()
|
||||||
|
d.attached = a
|
||||||
|
d.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if the this process is currently attached to the container.
|
||||||
|
func (d *DockerEnvironment) IsAttached() bool {
|
||||||
|
d.RLock()
|
||||||
|
defer d.RUnlock()
|
||||||
|
|
||||||
|
return d.attached
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a new base Docker environment. A server must still be attached to it.
|
||||||
|
func NewDockerEnvironment(server *Server) error {
|
||||||
|
cli, err := client.NewClientWithOpts(client.FromEnv)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
server.Environment = &DockerEnvironment{
|
||||||
|
Server: server,
|
||||||
|
Client: cli,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that the Docker environment is always implementing all of the methods
|
||||||
|
// from the base environment interface.
|
||||||
|
var _ Environment = (*DockerEnvironment)(nil)
|
||||||
|
|
||||||
|
// Returns the name of the environment.
|
||||||
|
func (d *DockerEnvironment) Type() string {
|
||||||
|
return "docker"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determines if the container exists in this environment.
|
||||||
|
func (d *DockerEnvironment) Exists() (bool, error) {
|
||||||
|
_, err := d.Client.ContainerInspect(context.Background(), d.Server.Id())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// If this error is because the container instance wasn't found via Docker we
|
||||||
|
// can safely ignore the error and just return false.
|
||||||
|
if client.IsErrNotFound(err) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determines if the server's docker container is currently running. If there is no container
|
||||||
|
// present, an error will be raised (since this shouldn't be a case that ever happens under
|
||||||
|
// correctly developed circumstances).
|
||||||
|
//
|
||||||
|
// You can confirm if the instance wasn't found by using client.IsErrNotFound from the Docker
|
||||||
|
// API.
|
||||||
|
//
|
||||||
|
// @see docker/client/errors.go
|
||||||
|
func (d *DockerEnvironment) IsRunning() (bool, error) {
|
||||||
|
c, err := d.Client.ContainerInspect(context.Background(), d.Server.Id())
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.State.Running, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performs an in-place update of the Docker container's resource limits without actually
|
||||||
|
// making any changes to the operational state of the container. This allows memory, cpu,
|
||||||
|
// and IO limitations to be adjusted on the fly for individual instances.
|
||||||
|
func (d *DockerEnvironment) InSituUpdate() error {
|
||||||
|
if _, err := d.Client.ContainerInspect(context.Background(), d.Server.Id()); err != nil {
|
||||||
|
// If the container doesn't exist for some reason there really isn't anything
|
||||||
|
// we can do to fix that in this process (it doesn't make sense at least). In those
|
||||||
|
// cases just return without doing anything since we still want to save the configuration
|
||||||
|
// to the disk.
|
||||||
|
//
|
||||||
|
// We'll let a boot process make modifications to the container if needed at this point.
|
||||||
|
if client.IsErrNotFound(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
u := container.UpdateConfig{
|
||||||
|
Resources: d.getResourcesForServer(),
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Server.Log().WithField("limits", fmt.Sprintf("%+v", u.Resources)).Debug("updating server container on-the-fly with passed limits")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||||
|
defer cancel()
|
||||||
|
if _, err := d.Client.ContainerUpdate(ctx, d.Server.Id(), u); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run before the container starts and get the process configuration from the Panel.
|
||||||
|
// This is important since we use this to check configuration files as well as ensure
|
||||||
|
// we always have the latest version of an egg available for server processes.
|
||||||
|
//
|
||||||
|
// This process will also confirm that the server environment exists and is in a bootable
|
||||||
|
// state. This ensures that unexpected container deletion while Wings is running does
|
||||||
|
// not result in the server becoming unbootable.
|
||||||
|
func (d *DockerEnvironment) OnBeforeStart() error {
|
||||||
|
d.Server.Log().Info("syncing server configuration with panel")
|
||||||
|
if err := d.Server.Sync(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !d.Server.Filesystem.HasSpaceAvailable() {
|
||||||
|
return errors.New("cannot start server, not enough disk space available")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always destroy and re-create the server container to ensure that synced data from
|
||||||
|
// the Panel is used.
|
||||||
|
if err := d.Client.ContainerRemove(context.Background(), d.Server.Id(), types.ContainerRemoveOptions{RemoveVolumes: true}); err != nil {
|
||||||
|
if !client.IsErrNotFound(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Create() function will check if the container exists in the first place, and if
|
||||||
|
// so just silently return without an error. Otherwise, it will try to create the necessary
|
||||||
|
// container and data storage directory.
|
||||||
|
//
|
||||||
|
// This won't actually run an installation process however, it is just here to ensure the
|
||||||
|
// environment gets created properly if it is missing and the server is started. We're making
|
||||||
|
// an assumption that all of the files will still exist at this point.
|
||||||
|
if err := d.Create(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starts the server environment and begins piping output to the event listeners for the
|
||||||
|
// console. If a container does not exist, or needs to be rebuilt that will happen in the
|
||||||
|
// call to OnBeforeStart().
|
||||||
|
func (d *DockerEnvironment) Start() error {
|
||||||
|
sawError := false
|
||||||
|
// If sawError is set to true there was an error somewhere in the pipeline that
|
||||||
|
// got passed up, but we also want to ensure we set the server to be offline at
|
||||||
|
// that point.
|
||||||
|
defer func() {
|
||||||
|
if sawError {
|
||||||
|
// If we don't set it to stopping first, you'll trigger crash detection which
|
||||||
|
// we don't want to do at this point since it'll just immediately try to do the
|
||||||
|
// exact same action that lead to it crashing in the first place...
|
||||||
|
d.Server.SetState(ProcessStoppingState)
|
||||||
|
d.Server.SetState(ProcessOfflineState)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// If the server is suspended the user shouldn't be able to boot it, in those cases
|
||||||
|
// return a suspension error and let the calling area handle the issue.
|
||||||
|
//
|
||||||
|
// Theoretically you'd have the Panel handle all of this logic, but we cannot do that
|
||||||
|
// because we allow the websocket to control the server power state as well, so we'll
|
||||||
|
// need to handle that action in here.
|
||||||
|
if d.Server.IsSuspended() {
|
||||||
|
return &suspendedError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c, err := d.Client.ContainerInspect(context.Background(), d.Server.Id()); err != nil {
|
||||||
|
// Do nothing if the container is not found, we just don't want to continue
|
||||||
|
// to the next block of code here. This check was inlined here to guard againt
|
||||||
|
// a nil-pointer when checking c.State below.
|
||||||
|
//
|
||||||
|
// @see https://github.com/pterodactyl/panel/issues/2000
|
||||||
|
if !client.IsErrNotFound(err) {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If the server is running update our internal state and continue on with the attach.
|
||||||
|
if c.State.Running {
|
||||||
|
d.Server.SetState(ProcessRunningState)
|
||||||
|
|
||||||
|
return d.Attach()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truncate the log file so we don't end up outputting a bunch of useless log information
|
||||||
|
// to the websocket and whatnot. Check first that the path and file exist before trying
|
||||||
|
// to truncate them.
|
||||||
|
if _, err := os.Stat(c.LogPath); err == nil {
|
||||||
|
if err := os.Truncate(c.LogPath, 0); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Server.SetState(ProcessStartingState)
|
||||||
|
// Set this to true for now, we will set it to false once we reach the
|
||||||
|
// end of this chain.
|
||||||
|
sawError = true
|
||||||
|
|
||||||
|
// Run the before start function and wait for it to finish. This will validate that the container
|
||||||
|
// exists on the system, and rebuild the container if that is required for server booting to
|
||||||
|
// occur.
|
||||||
|
if err := d.OnBeforeStart(); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the configuration files defined for the server before beginning the boot process.
|
||||||
|
// This process executes a bunch of parallel updates, so we just block until that process
|
||||||
|
// is completed. Any errors as a result of this will just be bubbled out in the logger,
|
||||||
|
// we don't need to actively do anything about it at this point, worst comes to worst the
|
||||||
|
// server starts in a weird state and the user can manually adjust.
|
||||||
|
d.Server.UpdateConfigurationFiles()
|
||||||
|
|
||||||
|
// Reset the permissions on files for the server before actually trying
|
||||||
|
// to start it.
|
||||||
|
if err := d.Server.Filesystem.Chown("/"); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := d.Client.ContainerStart(ctx, d.Server.Id(), types.ContainerStartOptions{}); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// No errors, good to continue through.
|
||||||
|
sawError = false
|
||||||
|
|
||||||
|
return d.Attach()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stops the container that the server is running in. This will allow up to 10
|
||||||
|
// seconds to pass before a failure occurs.
|
||||||
|
func (d *DockerEnvironment) Stop() error {
|
||||||
|
stop := d.Server.ProcessConfiguration().Stop
|
||||||
|
if stop.Type == api.ProcessStopSignal {
|
||||||
|
return d.Terminate(os.Kill)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Server.SetState(ProcessStoppingState)
|
||||||
|
// Only attempt to send the stop command to the instance if we are actually attached to
|
||||||
|
// the instance. If we are not for some reason, just send the container stop event.
|
||||||
|
if d.IsAttached() && stop.Type == api.ProcessStopCommand {
|
||||||
|
return d.SendCommand(stop.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
t := time.Second * 10
|
||||||
|
|
||||||
|
err := d.Client.ContainerStop(context.Background(), d.Server.Id(), &t)
|
||||||
|
if err != nil {
|
||||||
|
// If the container does not exist just mark the process as stopped and return without
|
||||||
|
// an error.
|
||||||
|
if client.IsErrNotFound(err) {
|
||||||
|
d.SetAttached(false)
|
||||||
|
d.Server.SetState(ProcessOfflineState)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to acquire a lock to restart the server. If one cannot be obtained within 5 seconds return
|
||||||
|
// an error to the caller. You should ideally be checking IsRestarting() before calling this function
|
||||||
|
// to avoid unnecessary delays since you can respond immediately from that.
|
||||||
|
func (d *DockerEnvironment) acquireRestartLock() error {
|
||||||
|
if d.restartSem == nil {
|
||||||
|
d.restartSem = semaphore.NewWeighted(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, _ := context.WithTimeout(context.Background(), time.Second*5)
|
||||||
|
|
||||||
|
return d.restartSem.Acquire(ctx, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restarts the server process by waiting for the process to gracefully stop and then triggering a
|
||||||
|
// start command. This will return an error if there is already a restart process executing for the
|
||||||
|
// server. The lock is released when the process is stopped and a start has begun.
|
||||||
|
func (d *DockerEnvironment) Restart() error {
|
||||||
|
d.Server.Log().Debug("attempting to acquire restart lock...")
|
||||||
|
if err := d.acquireRestartLock(); err != nil {
|
||||||
|
d.Server.Log().Warn("failed to acquire restart lock; already acquired by a different process")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Server.Log().Debug("acquired restart lock")
|
||||||
|
|
||||||
|
err := d.WaitForStop(60, false)
|
||||||
|
if err != nil {
|
||||||
|
d.restartSem.Release(1)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release the restart lock, it is now safe for someone to attempt restarting the server again.
|
||||||
|
d.restartSem.Release(1)
|
||||||
|
|
||||||
|
// Start the process.
|
||||||
|
return d.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the server is currently running the restart process by checking if there is a semaphore
|
||||||
|
// allocated, and if so, if we can aquire a lock on it.
|
||||||
|
func (d *DockerEnvironment) IsRestarting() bool {
|
||||||
|
if d.restartSem == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.restartSem.TryAcquire(1) {
|
||||||
|
d.restartSem.Release(1)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempts to gracefully stop a server using the defined stop command. If the server
|
||||||
|
// does not stop after seconds have passed, an error will be returned, or the instance
|
||||||
|
// will be terminated forcefully depending on the value of the second argument.
|
||||||
|
func (d *DockerEnvironment) WaitForStop(seconds int, terminate bool) error {
|
||||||
|
if d.Server.GetState() == ProcessOfflineState {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := d.Stop(); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(seconds)*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Block the return of this function until the container as been marked as no
|
||||||
|
// longer running. If this wait does not end by the time seconds have passed,
|
||||||
|
// attempt to terminate the container, or return an error.
|
||||||
|
ok, errChan := d.Client.ContainerWait(ctx, d.Server.Id(), container.WaitConditionNotRunning)
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||||
|
if terminate {
|
||||||
|
return d.Terminate(os.Kill)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.WithStack(ctxErr)
|
||||||
|
}
|
||||||
|
case err := <-errChan:
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
case <-ok:
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forcefully terminates the container using the signal passed through.
|
||||||
|
func (d *DockerEnvironment) Terminate(signal os.Signal) error {
|
||||||
|
c, err := d.Client.ContainerInspect(context.Background(), d.Server.Id())
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.State.Running {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Server.SetState(ProcessStoppingState)
|
||||||
|
|
||||||
|
return d.Client.ContainerKill(
|
||||||
|
context.Background(), d.Server.Id(), strings.TrimSuffix(strings.TrimPrefix(signal.String(), "signal "), "ed"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the Docker container from the machine. If the container is currently running
|
||||||
|
// it will be forcibly stopped by Docker.
|
||||||
|
func (d *DockerEnvironment) Destroy() error {
|
||||||
|
// Avoid crash detection firing off.
|
||||||
|
d.Server.SetState(ProcessStoppingState)
|
||||||
|
|
||||||
|
err := d.Client.ContainerRemove(context.Background(), d.Server.Id(), types.ContainerRemoveOptions{
|
||||||
|
RemoveVolumes: true,
|
||||||
|
RemoveLinks: false,
|
||||||
|
Force: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Don't trigger a destroy failure if we try to delete a container that does not
|
||||||
|
// exist on the system. We're just a step ahead of ourselves in that case.
|
||||||
|
//
|
||||||
|
// @see https://github.com/pterodactyl/panel/issues/2001
|
||||||
|
if err != nil && client.IsErrNotFound(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the container exit state and return the exit code and wether or not
|
||||||
|
// the container was killed by the OOM killer.
|
||||||
|
func (d *DockerEnvironment) ExitState() (uint32, bool, error) {
|
||||||
|
c, err := d.Client.ContainerInspect(context.Background(), d.Server.Id())
|
||||||
|
if err != nil {
|
||||||
|
// I'm not entirely sure how this can happen to be honest. I tried deleting a
|
||||||
|
// container _while_ a server was running and wings gracefully saw the crash and
|
||||||
|
// created a new container for it.
|
||||||
|
//
|
||||||
|
// However, someone reported an error in Discord about this scenario happening,
|
||||||
|
// so I guess this should prevent it? They didn't tell me how they caused it though
|
||||||
|
// so thats a mystery that will have to go unsolved.
|
||||||
|
//
|
||||||
|
// @see https://github.com/pterodactyl/panel/issues/2003
|
||||||
|
if client.IsErrNotFound(err) {
|
||||||
|
return 1, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, false, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return uint32(c.State.ExitCode), c.State.OOMKilled, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attaches to the docker container itself and ensures that we can pipe data in and out
|
||||||
|
// of the process stream. This should not be used for reading console data as you *will*
|
||||||
|
// miss important output at the beginning because of the time delay with attaching to the
|
||||||
|
// output.
|
||||||
|
func (d *DockerEnvironment) Attach() error {
|
||||||
|
if d.IsAttached() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := d.FollowConsoleOutput(); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
d.stream, err = d.Client.ContainerAttach(context.Background(), d.Server.Id(), types.ContainerAttachOptions{
|
||||||
|
Stdin: true,
|
||||||
|
Stdout: true,
|
||||||
|
Stderr: true,
|
||||||
|
Stream: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
console := Console{
|
||||||
|
Server: d.Server,
|
||||||
|
}
|
||||||
|
|
||||||
|
d.SetAttached(true)
|
||||||
|
go func() {
|
||||||
|
if err := d.EnableResourcePolling(); err != nil {
|
||||||
|
d.Server.Log().WithField("error", errors.WithStack(err)).Warn("failed to enable resource polling on server")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer d.stream.Close()
|
||||||
|
defer func() {
|
||||||
|
d.Server.SetState(ProcessOfflineState)
|
||||||
|
d.SetAttached(false)
|
||||||
|
}()
|
||||||
|
|
||||||
|
io.Copy(console, d.stream.Reader)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attaches to the log for the container. This avoids us missing cruicial output that
|
||||||
|
// happens in the split seconds before the code moves from 'Starting' to 'Attaching'
|
||||||
|
// on the process.
|
||||||
|
func (d *DockerEnvironment) FollowConsoleOutput() error {
|
||||||
|
if exists, err := d.Exists(); !exists {
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New(fmt.Sprintf("no such container: %s", d.Server.Id()))
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := types.ContainerLogsOptions{
|
||||||
|
ShowStderr: true,
|
||||||
|
ShowStdout: true,
|
||||||
|
Follow: true,
|
||||||
|
Since: time.Now().Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := d.Client.ContainerLogs(context.Background(), d.Server.Id(), opts)
|
||||||
|
|
||||||
|
go func(r io.ReadCloser) {
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
s := bufio.NewScanner(r)
|
||||||
|
for s.Scan() {
|
||||||
|
d.Server.Events().Publish(ConsoleOutputEvent, s.Text())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Err(); err != nil {
|
||||||
|
d.Server.Log().WithField("error", err).Warn("error processing scanner line in console output")
|
||||||
|
}
|
||||||
|
}(reader)
|
||||||
|
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enables resource polling on the docker instance. Except we aren't actually polling Docker for this
|
||||||
|
// information, instead just sit there with an async process that lets Docker stream all of this data
|
||||||
|
// to us automatically.
|
||||||
|
func (d *DockerEnvironment) EnableResourcePolling() error {
|
||||||
|
if d.Server.GetState() == ProcessOfflineState {
|
||||||
|
return errors.New("cannot enable resource polling on a server that is not running")
|
||||||
|
}
|
||||||
|
|
||||||
|
stats, err := d.Client.ContainerStats(context.Background(), d.Server.Id(), true)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
d.stats = stats.Body
|
||||||
|
|
||||||
|
dec := json.NewDecoder(d.stats)
|
||||||
|
go func(s *Server) {
|
||||||
|
for {
|
||||||
|
var v *types.StatsJSON
|
||||||
|
|
||||||
|
if err := dec.Decode(&v); err != nil {
|
||||||
|
if err != io.EOF {
|
||||||
|
d.Server.Log().WithField("error", err).Warn("encountered error processing server stats, stopping collection")
|
||||||
|
}
|
||||||
|
|
||||||
|
d.DisableResourcePolling()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable collection if the server is in an offline state and this process is
|
||||||
|
// still running.
|
||||||
|
if s.GetState() == ProcessOfflineState {
|
||||||
|
d.DisableResourcePolling()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Proc().UpdateFromDocker(v)
|
||||||
|
for _, nw := range v.Networks {
|
||||||
|
s.Proc().UpdateNetworkBytes(&nw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Why you ask? This already has the logic for caching disk space in use and then
|
||||||
|
// also handles pushing that value to the resources object automatically.
|
||||||
|
s.Filesystem.HasSpaceAvailable()
|
||||||
|
|
||||||
|
b, _ := json.Marshal(s.Proc())
|
||||||
|
s.Events().Publish(StatsEvent, string(b))
|
||||||
|
}
|
||||||
|
}(d.Server)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closes the stats stream for a server process.
|
||||||
|
func (d *DockerEnvironment) DisableResourcePolling() error {
|
||||||
|
if d.stats == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := d.stats.Close()
|
||||||
|
d.Server.Proc().Empty()
|
||||||
|
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the image to be used for the instance.
|
||||||
|
func (d *DockerEnvironment) Image() string {
|
||||||
|
return d.Server.Config().Container.Image
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pulls the image from Docker. If there is an error while pulling the image from the source
|
||||||
|
// but the image already exists locally, we will report that error to the logger but continue
|
||||||
|
// with the process.
|
||||||
|
//
|
||||||
|
// The reasoning behind this is that Quay has had some serious outages as of late, and we don't
|
||||||
|
// need to block all of the servers from booting just because of that. I'd imagine in a lot of
|
||||||
|
// cases an outage shouldn't affect users too badly. It'll at least keep existing servers working
|
||||||
|
// correctly if anything.
|
||||||
|
//
|
||||||
|
// @todo handle authorization & local images
|
||||||
|
func (d *DockerEnvironment) ensureImageExists() error {
|
||||||
|
// Give it up to 15 minutes to pull the image. I think this should cover 99.8% of cases where an
|
||||||
|
// image pull might fail. I can't imagine it will ever take more than 15 minutes to fully pull
|
||||||
|
// an image. Let me know when I am inevitably wrong here...
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*15)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
out, err := d.Client.ImagePull(ctx, d.Image(), types.ImagePullOptions{All: false})
|
||||||
|
if err != nil {
|
||||||
|
images, ierr := d.Client.ImageList(ctx, types.ImageListOptions{})
|
||||||
|
if ierr != nil {
|
||||||
|
// Well damn, something has gone really wrong here, just go ahead and abort there
|
||||||
|
// isn't much anything we can do to try and self-recover from this.
|
||||||
|
return ierr
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, img := range images {
|
||||||
|
for _, t := range img.RepoTags {
|
||||||
|
if t != d.Image() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Server.Log().WithFields(log.Fields{
|
||||||
|
"image": d.Image(),
|
||||||
|
"error": errors.New(err.Error()),
|
||||||
|
}).Warn("unable to pull requested image from remote source, however the image exists locally")
|
||||||
|
|
||||||
|
// Okay, we found a matching container image, in that case just go ahead and return
|
||||||
|
// from this function, since there is nothing else we need to do here.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
log.WithField("image", d.Image()).Debug("pulling docker image... this could take a bit of time")
|
||||||
|
|
||||||
|
// I'm not sure what the best approach here is, but this will block execution until the image
|
||||||
|
// is done being pulled, which is what we need.
|
||||||
|
scanner := bufio.NewScanner(out)
|
||||||
|
for scanner.Scan() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a new container for the server using all of the data that is currently
|
||||||
|
// available for it. If the container already exists it will be returned.
|
||||||
|
func (d *DockerEnvironment) Create() error {
|
||||||
|
// Ensure the data directory exists before getting too far through this process.
|
||||||
|
if err := d.Server.Filesystem.EnsureDataDirectory(); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the container already exists don't hit the user with an error, just return
|
||||||
|
// the current information about it which is what we would do when creating the
|
||||||
|
// container anyways.
|
||||||
|
if _, err := d.Client.ContainerInspect(context.Background(), d.Server.Id()); err == nil {
|
||||||
|
return nil
|
||||||
|
} else if !client.IsErrNotFound(err) {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to pull the requested image before creating the container.
|
||||||
|
if err := d.ensureImageExists(); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
conf := &container.Config{
|
||||||
|
Hostname: d.Server.Id(),
|
||||||
|
Domainname: config.Get().Docker.Domainname,
|
||||||
|
User: strconv.Itoa(config.Get().System.User.Uid),
|
||||||
|
AttachStdin: true,
|
||||||
|
AttachStdout: true,
|
||||||
|
AttachStderr: true,
|
||||||
|
OpenStdin: true,
|
||||||
|
Tty: true,
|
||||||
|
ExposedPorts: d.exposedPorts(),
|
||||||
|
Image: d.Image(),
|
||||||
|
Env: d.Server.GetEnvironmentVariables(),
|
||||||
|
Labels: map[string]string{
|
||||||
|
"Service": "Pterodactyl",
|
||||||
|
"ContainerType": "server_process",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mounts := []mount.Mount{
|
||||||
|
{
|
||||||
|
Target: "/home/container",
|
||||||
|
Source: d.Server.Filesystem.Path(),
|
||||||
|
Type: mount.TypeBind,
|
||||||
|
ReadOnly: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var mounted bool
|
||||||
|
for _, m := range d.Server.Config().Mounts {
|
||||||
|
mounted = false
|
||||||
|
source := filepath.Clean(m.Source)
|
||||||
|
target := filepath.Clean(m.Target)
|
||||||
|
|
||||||
|
for _, allowed := range config.Get().AllowedMounts {
|
||||||
|
if !strings.HasPrefix(source, allowed) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
mounts = append(mounts, mount.Mount{
|
||||||
|
Type: mount.TypeBind,
|
||||||
|
|
||||||
|
Source: source,
|
||||||
|
Target: target,
|
||||||
|
ReadOnly: m.ReadOnly,
|
||||||
|
})
|
||||||
|
|
||||||
|
mounted = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := log.WithFields(log.Fields{
|
||||||
|
"server": d.Server.Id(),
|
||||||
|
"source_path": source,
|
||||||
|
"target_path": target,
|
||||||
|
"read_only": m.ReadOnly,
|
||||||
|
})
|
||||||
|
|
||||||
|
if mounted {
|
||||||
|
logger.Debug("attaching mount to server's container")
|
||||||
|
} else {
|
||||||
|
logger.Warn("skipping mount because it isn't allowed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hostConf := &container.HostConfig{
|
||||||
|
PortBindings: d.portBindings(),
|
||||||
|
|
||||||
|
// Configure the mounts for this container. First mount the server data directory
|
||||||
|
// into the container as a r/w bind.
|
||||||
|
Mounts: mounts,
|
||||||
|
|
||||||
|
// Configure the /tmp folder mapping in containers. This is necessary for some
|
||||||
|
// games that need to make use of it for downloads and other installation processes.
|
||||||
|
Tmpfs: map[string]string{
|
||||||
|
"/tmp": "rw,exec,nosuid,size=50M",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Define resource limits for the container based on the data passed through
|
||||||
|
// from the Panel.
|
||||||
|
Resources: d.getResourcesForServer(),
|
||||||
|
|
||||||
|
DNS: config.Get().Docker.Network.Dns,
|
||||||
|
|
||||||
|
// Configure logging for the container to make it easier on the Daemon to grab
|
||||||
|
// the server output. Ensure that we don't use too much space on the host machine
|
||||||
|
// since we only need it for the last few hundred lines of output and don't care
|
||||||
|
// about anything else in it.
|
||||||
|
LogConfig: container.LogConfig{
|
||||||
|
Type: jsonfilelog.Name,
|
||||||
|
Config: map[string]string{
|
||||||
|
"max-size": "5m",
|
||||||
|
"max-file": "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
SecurityOpt: []string{"no-new-privileges"},
|
||||||
|
ReadonlyRootfs: true,
|
||||||
|
CapDrop: []string{
|
||||||
|
"setpcap", "mknod", "audit_write", "net_raw", "dac_override",
|
||||||
|
"fowner", "fsetid", "net_bind_service", "sys_chroot", "setfcap",
|
||||||
|
},
|
||||||
|
NetworkMode: container.NetworkMode(config.Get().Docker.Network.Mode),
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := d.Client.ContainerCreate(context.Background(), conf, hostConf, nil, d.Server.Id()); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sends the specified command to the stdin of the running container instance. There is no
|
||||||
|
// confirmation that this data is sent successfully, only that it gets pushed into the stdin.
|
||||||
|
func (d *DockerEnvironment) SendCommand(c string) error {
|
||||||
|
if !d.IsAttached() {
|
||||||
|
return errors.New("attempting to send command to non-attached instance")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := d.stream.Conn.Write([]byte(c + "\n"))
|
||||||
|
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reads the log file for the server. This does not care if the server is running or not, it will
|
||||||
|
// simply try to read the last X bytes of the file and return them.
|
||||||
|
func (d *DockerEnvironment) Readlog(len int64) ([]string, error) {
|
||||||
|
j, err := d.Client.ContainerInspect(context.Background(), d.Server.Id())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if j.LogPath == "" {
|
||||||
|
return nil, errors.New("empty log path defined for server")
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(j.LogPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
// Check if the length of the file is smaller than the amount of data that was requested
|
||||||
|
// for reading. If so, adjust the length to be the total length of the file. If this is not
|
||||||
|
// done an error is thrown since we're reading backwards, and not forwards.
|
||||||
|
if stat, err := os.Stat(j.LogPath); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if stat.Size() < len {
|
||||||
|
len = stat.Size()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed to the end of the file and then move backwards until the length is met to avoid
|
||||||
|
// reading the entirety of the file into memory.
|
||||||
|
if _, err := f.Seek(-len, io.SeekEnd); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b := make([]byte, len)
|
||||||
|
|
||||||
|
if _, err := f.Read(b); err != nil && err != io.EOF {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.parseLogToStrings(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
type dockerLogLine struct {
|
||||||
|
Log string `json:"log"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Docker stores the logs for server output in a JSON format. This function will iterate over the JSON
|
||||||
|
// that was read from the log file and parse it into a more human readable format.
|
||||||
|
func (d *DockerEnvironment) parseLogToStrings(b []byte) ([]string, error) {
|
||||||
|
var hasError = false
|
||||||
|
var out []string
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(bytes.NewReader(b))
|
||||||
|
for scanner.Scan() {
|
||||||
|
var l dockerLogLine
|
||||||
|
// Unmarshal the contents and allow up to a single error before bailing out of the process. We
|
||||||
|
// do this because if you're arbitrarily reading a length of the file you'll likely end up
|
||||||
|
// with the first line in the output being improperly formatted JSON. In those cases we want to
|
||||||
|
// just skip over it. However if we see another error we're going to bail out because that is an
|
||||||
|
// abnormal situation.
|
||||||
|
if err := json.Unmarshal([]byte(scanner.Text()), &l); err != nil {
|
||||||
|
if hasError {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
hasError = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
out = append(out, l.Log)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts the server allocation mappings into a format that can be understood
|
||||||
|
// by Docker.
|
||||||
|
func (d *DockerEnvironment) portBindings() nat.PortMap {
|
||||||
|
var out = nat.PortMap{}
|
||||||
|
|
||||||
|
for ip, ports := range d.Server.Config().Allocations.Mappings {
|
||||||
|
for _, port := range ports {
|
||||||
|
// Skip over invalid ports.
|
||||||
|
if port < 0 || port > 65535 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
binding := []nat.PortBinding{
|
||||||
|
{
|
||||||
|
HostIP: ip,
|
||||||
|
HostPort: strconv.Itoa(port),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
out[nat.Port(fmt.Sprintf("%d/tcp", port))] = binding
|
||||||
|
out[nat.Port(fmt.Sprintf("%d/udp", port))] = binding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converts the server allocation mappings into a PortSet that can be understood
|
||||||
|
// by Docker. This formatting is slightly different than portBindings as it should
|
||||||
|
// return an empty struct rather than a binding.
|
||||||
|
//
|
||||||
|
// To accomplish this, we'll just get the values from portBindings and then set them
|
||||||
|
// to empty structs. Because why not.
|
||||||
|
func (d *DockerEnvironment) exposedPorts() nat.PortSet {
|
||||||
|
var out = nat.PortSet{}
|
||||||
|
|
||||||
|
for port := range d.portBindings() {
|
||||||
|
out[port] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formats the resources available to a server instance in such as way that Docker will
|
||||||
|
// generate a matching environment in the container.
|
||||||
|
//
|
||||||
|
// This will set the actual memory limit on the container using the multiplier which is the
|
||||||
|
// hard limit for the container (after which will result in a crash). We then set the
|
||||||
|
// reservation to be the expected memory limit based on simply multiplication.
|
||||||
|
//
|
||||||
|
// The swap value is either -1 to disable it, or set to the value of the hard memory limit
|
||||||
|
// plus the additional swap assigned to the server since Docker expects this value to be
|
||||||
|
// the same or higher than the memory limit.
|
||||||
|
func (d *DockerEnvironment) getResourcesForServer() container.Resources {
|
||||||
|
return container.Resources{
|
||||||
|
Memory: d.Server.Build().BoundedMemoryLimit(),
|
||||||
|
MemoryReservation: d.Server.Build().MemoryLimit * 1_000_000,
|
||||||
|
MemorySwap: d.Server.Build().ConvertedSwap(),
|
||||||
|
CPUQuota: d.Server.Build().ConvertedCpuLimit(),
|
||||||
|
CPUPeriod: 100_000,
|
||||||
|
CPUShares: 1024,
|
||||||
|
BlkioWeight: d.Server.Build().IoWeight,
|
||||||
|
OomKillDisable: &d.Server.Config().Container.OomDisabled,
|
||||||
|
CpusetCpus: d.Server.Build().Threads,
|
||||||
|
}
|
||||||
|
}
|
||||||
106
server/events.go
106
server/events.go
@@ -1,7 +1,9 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/pterodactyl/wings/events"
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Defines all of the possible output events for a server.
|
// Defines all of the possible output events for a server.
|
||||||
@@ -9,22 +11,110 @@ import (
|
|||||||
const (
|
const (
|
||||||
DaemonMessageEvent = "daemon message"
|
DaemonMessageEvent = "daemon message"
|
||||||
InstallOutputEvent = "install output"
|
InstallOutputEvent = "install output"
|
||||||
InstallStartedEvent = "install started"
|
|
||||||
InstallCompletedEvent = "install completed"
|
|
||||||
ConsoleOutputEvent = "console output"
|
ConsoleOutputEvent = "console output"
|
||||||
StatusEvent = "status"
|
StatusEvent = "status"
|
||||||
StatsEvent = "stats"
|
StatsEvent = "stats"
|
||||||
BackupCompletedEvent = "backup completed"
|
BackupCompletedEvent = "backup completed"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Returns the server's emitter instance.
|
type Event struct {
|
||||||
func (s *Server) Events() *events.EventBus {
|
Data string
|
||||||
s.emitterLock.Lock()
|
Topic string
|
||||||
defer s.emitterLock.Unlock()
|
}
|
||||||
|
|
||||||
|
type EventBus struct {
|
||||||
|
sync.RWMutex
|
||||||
|
|
||||||
|
subscribers map[string][]chan Event
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the server's emitter instance.
|
||||||
|
func (s *Server) Events() *EventBus {
|
||||||
if s.emitter == nil {
|
if s.emitter == nil {
|
||||||
s.emitter = events.New()
|
s.emitter = &EventBus{
|
||||||
|
subscribers: map[string][]chan Event{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.emitter
|
return s.emitter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Publish data to a given topic.
|
||||||
|
func (e *EventBus) Publish(topic string, data string) {
|
||||||
|
e.RLock()
|
||||||
|
defer e.RUnlock()
|
||||||
|
|
||||||
|
t := topic
|
||||||
|
// Some of our topics for the socket support passing a more specific namespace,
|
||||||
|
// such as "backup completed:1234" to indicate which specific backup was completed.
|
||||||
|
//
|
||||||
|
// In these cases, we still need to the send the event using the standard listener
|
||||||
|
// name of "backup completed".
|
||||||
|
if strings.Contains(topic, ":") {
|
||||||
|
parts := strings.SplitN(topic, ":", 2)
|
||||||
|
|
||||||
|
if len(parts) == 2 {
|
||||||
|
t = parts[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ch, ok := e.subscribers[t]; ok {
|
||||||
|
go func(data Event, cs []chan Event) {
|
||||||
|
for _, channel := range cs {
|
||||||
|
channel <- data
|
||||||
|
}
|
||||||
|
}(Event{Data: data, Topic: topic}, ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *EventBus) PublishJson(topic string, data interface{}) error {
|
||||||
|
b, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Publish(topic, string(b))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to an emitter topic using a channel.
|
||||||
|
func (e *EventBus) Subscribe(topic string, ch chan Event) {
|
||||||
|
e.Lock()
|
||||||
|
defer e.Unlock()
|
||||||
|
|
||||||
|
if p, ok := e.subscribers[topic]; ok {
|
||||||
|
e.subscribers[topic] = append(p, ch)
|
||||||
|
} else {
|
||||||
|
e.subscribers[topic] = append([]chan Event{}, ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsubscribe a channel from a topic.
|
||||||
|
func (e *EventBus) Unsubscribe(topic string, ch chan Event) {
|
||||||
|
e.Lock()
|
||||||
|
defer e.Unlock()
|
||||||
|
|
||||||
|
if _, ok := e.subscribers[topic]; ok {
|
||||||
|
for i := range e.subscribers[topic] {
|
||||||
|
if ch == e.subscribers[topic][i] {
|
||||||
|
e.subscribers[topic] = append(e.subscribers[topic][:i], e.subscribers[topic][i+1:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removes all of the event listeners for the server. This is used when a server
|
||||||
|
// is being deleted to avoid a bunch of de-reference errors cropping up. Obviously
|
||||||
|
// should also check elsewhere and handle a server reference going nil, but this
|
||||||
|
// won't hurt.
|
||||||
|
func (e *EventBus) UnsubscribeAll() {
|
||||||
|
e.Lock()
|
||||||
|
defer e.Unlock()
|
||||||
|
|
||||||
|
// Loop over all of the subscribers and just remove all of the events
|
||||||
|
// for them.
|
||||||
|
for t := range e.subscribers {
|
||||||
|
e.subscribers[t] = make([]chan Event, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gabriel-vasile/mimetype"
|
"github.com/gabriel-vasile/mimetype"
|
||||||
"github.com/karrick/godirwalk"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
"github.com/pterodactyl/wings/server/backup"
|
"github.com/pterodactyl/wings/server/backup"
|
||||||
@@ -22,7 +22,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -41,14 +40,8 @@ func IsPathResolutionError(err error) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Filesystem struct {
|
type Filesystem struct {
|
||||||
mu sync.Mutex
|
|
||||||
lookupTimeMu sync.RWMutex
|
|
||||||
|
|
||||||
lastLookupTime time.Time
|
|
||||||
lookupInProgress int32
|
|
||||||
disk int64
|
|
||||||
|
|
||||||
Server *Server
|
Server *Server
|
||||||
|
cacheDiskMu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the root path that contains all of a server's data.
|
// Returns the root path that contains all of a server's data.
|
||||||
@@ -121,10 +114,10 @@ func (fs *Filesystem) SafePath(p string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate a path to the file by cleaning it up and appending the root server path to it. This
|
// Generate a path to the file by cleaning it up and appending the root server path to it. This
|
||||||
// DOES NOT guarantee that the file resolves within the server data directory. You'll want to use
|
// DOES NOT gaurantee that the file resolves within the server data directory. You'll want to use
|
||||||
// the fs.unsafeIsInDataDirectory(p) function to confirm.
|
// the fs.unsafeIsInDataDirectory(p) function to confirm.
|
||||||
func (fs *Filesystem) unsafeFilePath(p string) string {
|
func (fs *Filesystem) unsafeFilePath(p string) string {
|
||||||
// Calling filepath.Clean on the joined directory will resolve it to the absolute path,
|
// Calling filpath.Clean on the joined directory will resolve it to the absolute path,
|
||||||
// removing any ../ type of resolution arguments, and leaving us with a direct path link.
|
// removing any ../ type of resolution arguments, and leaving us with a direct path link.
|
||||||
//
|
//
|
||||||
// This will also trim the existing root path off the beginning of the path passed to
|
// This will also trim the existing root path off the beginning of the path passed to
|
||||||
@@ -184,14 +177,14 @@ func (fs *Filesystem) ParallelSafePath(paths []string) ([]string, error) {
|
|||||||
pi := p
|
pi := p
|
||||||
|
|
||||||
// Recursively call this function to continue digging through the directory tree within
|
// Recursively call this function to continue digging through the directory tree within
|
||||||
// a separate goroutine. If the context is canceled abort this process.
|
// a seperate goroutine. If the context is canceled abort this process.
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
default:
|
default:
|
||||||
// If the callback returns true, go ahead and keep walking deeper. This allows
|
// If the callback returns true, go ahead and keep walking deeper. This allows
|
||||||
// us to programmatically continue deeper into directories, or stop digging
|
// us to programatically continue deeper into directories, or stop digging
|
||||||
// if that pathway knows it needs nothing else.
|
// if that pathway knows it needs nothing else.
|
||||||
if c, err := fs.SafePath(pi); err != nil {
|
if c, err := fs.SafePath(pi); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -208,20 +201,15 @@ func (fs *Filesystem) ParallelSafePath(paths []string) ([]string, error) {
|
|||||||
return cleaned, g.Wait()
|
return cleaned, g.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
type SpaceCheckingOpts struct {
|
|
||||||
AllowStaleResponse bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determines if the directory a file is trying to be added to has enough space available
|
// Determines if the directory a file is trying to be added to has enough space available
|
||||||
// for the file to be written to.
|
// for the file to be written to.
|
||||||
//
|
//
|
||||||
// Because determining the amount of space being used by a server is a taxing operation we
|
// 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.
|
// will load it all up into a cache and pull from that as long as the key is not expired.
|
||||||
//
|
func (fs *Filesystem) HasSpaceAvailable() bool {
|
||||||
// This operation will potentially block unless allowStaleValue is set to true. See the
|
space := fs.Server.Build().DiskSpace
|
||||||
// documentation on DiskUsage for how this affects the call.
|
|
||||||
func (fs *Filesystem) HasSpaceAvailable(allowStaleValue bool) bool {
|
size, err := fs.getCachedDiskUsage()
|
||||||
size, err := fs.DiskUsage(allowStaleValue)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.Server.Log().WithField("error", err).Warn("failed to determine root server directory size")
|
fs.Server.Log().WithField("error", err).Warn("failed to determine root server directory size")
|
||||||
}
|
}
|
||||||
@@ -230,7 +218,6 @@ func (fs *Filesystem) HasSpaceAvailable(allowStaleValue bool) bool {
|
|||||||
// been allocated.
|
// been allocated.
|
||||||
fs.Server.Proc().SetDisk(size)
|
fs.Server.Proc().SetDisk(size)
|
||||||
|
|
||||||
space := fs.Server.DiskSpace()
|
|
||||||
// If space is -1 or 0 just return true, means they're allowed unlimited.
|
// 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 it's limit
|
// Technically we could skip disk space calculation because we don't need to check if the server exceeds it's limit
|
||||||
@@ -247,52 +234,19 @@ func (fs *Filesystem) HasSpaceAvailable(allowStaleValue bool) bool {
|
|||||||
// as needed without overly taxing the system. This will prioritize the value from the cache to avoid
|
// 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
|
// excessive IO usage. We will only walk the filesystem and determine the size of the directory if there
|
||||||
// is no longer a cached value.
|
// is no longer a cached value.
|
||||||
//
|
func (fs *Filesystem) getCachedDiskUsage() (int64, error) {
|
||||||
// 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.
|
|
||||||
func (fs *Filesystem) DiskUsage(allowStaleValue bool) (int64, error) {
|
|
||||||
// Check if cache is expired.
|
|
||||||
fs.lookupTimeMu.RLock()
|
|
||||||
isValidInCache := fs.lastLookupTime.After(time.Now().Add(time.Second * -10))
|
|
||||||
fs.lookupTimeMu.RUnlock()
|
|
||||||
|
|
||||||
if !isValidInCache {
|
|
||||||
// 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 atomic.LoadInt32(&fs.lookupInProgress) == 0 {
|
|
||||||
// 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.
|
|
||||||
go func(fs *Filesystem) {
|
|
||||||
if _, err := fs.updateCachedDiskUsage(); err != nil {
|
|
||||||
fs.Server.Log().WithField("error", errors.WithStack(err)).Warn("failed to determine disk usage in go-routine")
|
|
||||||
}
|
|
||||||
}(fs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the currently cached value back to the calling function.
|
|
||||||
return atomic.LoadInt64(&fs.disk), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Updates the currently used disk space for a server.
|
|
||||||
func (fs *Filesystem) updateCachedDiskUsage() (int64, error) {
|
|
||||||
// Obtain an exclusive lock on this process so that we don't unintentionally run it at the same
|
// Obtain an exclusive lock on this process so that we don't unintentionally run it at the same
|
||||||
// time as another running process. Once the lock is available it'll read from the cache for the
|
// time as another running process. Once the lock is available it'll read from the cache for the
|
||||||
// second call rather than hitting the disk in parallel.
|
// second call rather than hitting the disk in parallel.
|
||||||
fs.mu.Lock()
|
//
|
||||||
defer fs.mu.Unlock()
|
// This effectively the same speed as running this call in parallel since this cache will return
|
||||||
|
// instantly on the second call.
|
||||||
|
fs.cacheDiskMu.Lock()
|
||||||
|
defer fs.cacheDiskMu.Unlock()
|
||||||
|
|
||||||
// Signal that we're currently updating the disk size so that other calls to the disk checking
|
if x, exists := fs.Server.cache.Get("disk_used"); exists {
|
||||||
// functions can determine if they should queue up additional calls to this function. Ensure that
|
return x.(int64), nil
|
||||||
// we always set this back to 0 when this process is done executing.
|
}
|
||||||
atomic.StoreInt32(&fs.lookupInProgress, 1)
|
|
||||||
defer atomic.StoreInt32(&fs.lookupInProgress, 0)
|
|
||||||
|
|
||||||
// If there is no size its either because there is no data (in which case running this function
|
// If there is no size its either because there is no data (in which case running this function
|
||||||
// will have effectively no impact), or there is nothing in the cache, in which case we need to
|
// will have effectively no impact), or there is nothing in the cache, in which case we need to
|
||||||
@@ -303,11 +257,7 @@ func (fs *Filesystem) updateCachedDiskUsage() (int64, error) {
|
|||||||
// Always cache the size, even if there is an error. We want to always return that value
|
// Always cache the size, even if there is an error. We want to always return that value
|
||||||
// so that we don't cause an endless loop of determining the disk size if there is a temporary
|
// so that we don't cause an endless loop of determining the disk size if there is a temporary
|
||||||
// error encountered.
|
// error encountered.
|
||||||
fs.lookupTimeMu.Lock()
|
fs.Server.cache.Set("disk_used", size, time.Second*60)
|
||||||
fs.lastLookupTime = time.Now()
|
|
||||||
fs.lookupTimeMu.Unlock()
|
|
||||||
|
|
||||||
atomic.StoreInt64(&fs.disk, size)
|
|
||||||
|
|
||||||
return size, err
|
return size, err
|
||||||
}
|
}
|
||||||
@@ -316,40 +266,20 @@ func (fs *Filesystem) updateCachedDiskUsage() (int64, error) {
|
|||||||
// through all of the folders. Returns the size in bytes. This can be a fairly taxing operation
|
// through all of the folders. Returns the size in bytes. This can be a fairly taxing operation
|
||||||
// on locations with tons of files, so it is recommended that you cache the output.
|
// on locations with tons of files, so it is recommended that you cache the output.
|
||||||
func (fs *Filesystem) DirectorySize(dir string) (int64, error) {
|
func (fs *Filesystem) DirectorySize(dir string) (int64, error) {
|
||||||
d, err := fs.SafePath(dir)
|
|
||||||
if err != nil {
|
|
||||||
return 0, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var size int64
|
var size int64
|
||||||
var st syscall.Stat_t
|
err := fs.Walk(dir, func(_ string, f os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
err = godirwalk.Walk(d, &godirwalk.Options{
|
return fs.handleWalkerError(err, f)
|
||||||
Unsorted: true,
|
|
||||||
Callback: func(p string, e *godirwalk.Dirent) error {
|
|
||||||
// If this is a symlink then resolve the final destination of it before trying to continue walking
|
|
||||||
// over its contents. If it resolves outside the server data directory just skip everything else for
|
|
||||||
// it. Otherwise, allow it to continue.
|
|
||||||
if e.IsSymlink() {
|
|
||||||
if _, err := fs.SafePath(p); err != nil {
|
|
||||||
if IsPathResolutionError(err) {
|
|
||||||
return godirwalk.SkipThis
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
if !f.IsDir() {
|
||||||
}
|
atomic.AddInt64(&size, f.Size())
|
||||||
}
|
|
||||||
|
|
||||||
if !e.IsDir() {
|
|
||||||
syscall.Lstat(p, &st)
|
|
||||||
atomic.AddInt64(&size, st.Size)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return size, errors.WithStack(err)
|
return size, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reads a file on the system and returns it as a byte representation in a file
|
// Reads a file on the system and returns it as a byte representation in a file
|
||||||
@@ -370,21 +300,17 @@ func (fs *Filesystem) Readfile(p string) (io.Reader, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Writes a file to the system. If the file does not already exist one will be created.
|
// Writes a file to the system. If the file does not already exist one will be created.
|
||||||
|
//
|
||||||
|
// @todo should probably have a write lock here so we don't write twice at once.
|
||||||
func (fs *Filesystem) Writefile(p string, r io.Reader) error {
|
func (fs *Filesystem) Writefile(p string, r io.Reader) error {
|
||||||
cleaned, err := fs.SafePath(p)
|
cleaned, err := fs.SafePath(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentSize int64
|
|
||||||
|
|
||||||
// If the file does not exist on the system already go ahead and create the pathway
|
// If the file does not exist on the system already go ahead and create the pathway
|
||||||
// to it and an empty file. We'll then write to it later on after this completes.
|
// to it and an empty file. We'll then write to it later on after this completes.
|
||||||
if stat, err := os.Stat(cleaned); err != nil {
|
if stat, err := os.Stat(cleaned); err != nil && os.IsNotExist(err) {
|
||||||
if !os.IsNotExist(err) {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Dir(cleaned), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(cleaned), 0755); err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
@@ -392,12 +318,10 @@ func (fs *Filesystem) Writefile(p string, r io.Reader) error {
|
|||||||
if err := fs.Chown(filepath.Dir(cleaned)); err != nil {
|
if err := fs.Chown(filepath.Dir(cleaned)); err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
} else {
|
} else if err != nil {
|
||||||
if stat.IsDir() {
|
return errors.WithStack(err)
|
||||||
return errors.New("cannot write file contents to a directory")
|
} else if stat.IsDir() {
|
||||||
}
|
return errors.New("cannot use a directory as a file for writing")
|
||||||
|
|
||||||
currentSize = stat.Size()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This will either create the file if it does not already exist, or open and
|
// This will either create the file if it does not already exist, or open and
|
||||||
@@ -408,11 +332,29 @@ func (fs *Filesystem) Writefile(p string, r io.Reader) error {
|
|||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
buf := make([]byte, 1024*4)
|
// Create a new buffered writer that will write to the file we just opened
|
||||||
sz, err := io.CopyBuffer(file, r, buf)
|
// and stream in the contents from the reader.
|
||||||
|
w := bufio.NewWriter(file)
|
||||||
|
|
||||||
// Adjust the disk usage to account for the old size and the new size of the file.
|
buf := make([]byte, 1024)
|
||||||
atomic.AddInt64(&fs.disk, sz-currentSize)
|
for {
|
||||||
|
n, err := r.Read(buf)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := w.Write(buf[:n]); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.Flush(); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
// Finally, chown the file to ensure the permissions don't end up out-of-whack
|
// Finally, chown the file to ensure the permissions don't end up out-of-whack
|
||||||
// if we had just created it.
|
// if we had just created it.
|
||||||
@@ -466,9 +408,9 @@ func (fs *Filesystem) unsafeStat(p string) (*Stat, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var m *mimetype.MIME
|
var m = "inode/directory"
|
||||||
if !s.IsDir() {
|
if !s.IsDir() {
|
||||||
m, err = mimetype.DetectFile(p)
|
m, _, err = mimetype.DetectFile(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -476,17 +418,13 @@ func (fs *Filesystem) unsafeStat(p string) (*Stat, error) {
|
|||||||
|
|
||||||
st := &Stat{
|
st := &Stat{
|
||||||
Info: s,
|
Info: s,
|
||||||
Mimetype: "inode/directory",
|
Mimetype: m,
|
||||||
}
|
|
||||||
|
|
||||||
if m != nil {
|
|
||||||
st.Mimetype = m.String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return st, nil
|
return st, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new directory (name) at a specified path (p) for the server.
|
// Creates a new directory (name) at a specificied path (p) for the server.
|
||||||
func (fs *Filesystem) CreateDirectory(name string, p string) error {
|
func (fs *Filesystem) CreateDirectory(name string, p string) error {
|
||||||
cleaned, err := fs.SafePath(path.Join(p, name))
|
cleaned, err := fs.SafePath(path.Join(p, name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -508,71 +446,87 @@ func (fs *Filesystem) Rename(from string, to string) error {
|
|||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the target file or directory already exists the rename function will fail, so just
|
if f, err := os.Stat(cleanedFrom); err != nil {
|
||||||
// bail out now.
|
return errors.WithStack(err)
|
||||||
if _, err := os.Stat(cleanedTo); err == nil {
|
} else {
|
||||||
return os.ErrExist
|
d := cleanedTo
|
||||||
|
if !f.IsDir() {
|
||||||
|
d = strings.TrimSuffix(d, path.Base(cleanedTo))
|
||||||
}
|
}
|
||||||
|
|
||||||
if cleanedTo == fs.Path() {
|
// Ensure that the directory we're moving into exists correctly on the system.
|
||||||
return errors.New("attempting to rename into an invalid directory space")
|
|
||||||
}
|
|
||||||
|
|
||||||
d := strings.TrimSuffix(cleanedTo, path.Base(cleanedTo))
|
|
||||||
// Ensure that the directory we're moving into exists correctly on the system. Only do this if
|
|
||||||
// we're not at the root directory level.
|
|
||||||
if d != fs.Path() {
|
|
||||||
if mkerr := os.MkdirAll(d, 0644); mkerr != nil {
|
if mkerr := os.MkdirAll(d, 0644); mkerr != nil {
|
||||||
return errors.Wrap(mkerr, "failed to create directory structure for file rename")
|
return errors.WithStack(mkerr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.Rename(cleanedFrom, cleanedTo)
|
return os.Rename(cleanedFrom, cleanedTo)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursively iterates over a file or directory and sets the permissions on all of the
|
// Recursively iterates over a directory and sets the permissions on all of the
|
||||||
// underlying files. Iterate over all of the files and directories. If it is a file just
|
// underlying files.
|
||||||
// go ahead and perform the chown operation. Otherwise dig deeper into the directory until
|
|
||||||
// we've run out of directories to dig into.
|
|
||||||
func (fs *Filesystem) Chown(path string) error {
|
func (fs *Filesystem) Chown(path string) error {
|
||||||
cleaned, err := fs.SafePath(path)
|
cleaned, err := fs.SafePath(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
uid := config.Get().System.User.Uid
|
if s, err := os.Stat(cleaned); err != nil {
|
||||||
gid := config.Get().System.User.Gid
|
return errors.WithStack(err)
|
||||||
|
} else if !s.IsDir() {
|
||||||
|
return os.Chown(cleaned, config.Get().System.User.Uid, config.Get().System.User.Gid)
|
||||||
|
}
|
||||||
|
|
||||||
// Start by just chowning the initial path that we received.
|
return fs.chownDirectory(cleaned)
|
||||||
if err := os.Chown(cleaned, uid, gid); err != nil {
|
}
|
||||||
|
|
||||||
|
// Iterate over all of the files and directories. If it is a file just go ahead and perform
|
||||||
|
// the chown operation. Otherwise dig deeper into the directory until we've run out of
|
||||||
|
// directories to dig into.
|
||||||
|
func (fs *Filesystem) chownDirectory(path string) error {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
cleaned, err := fs.SafePath(path)
|
||||||
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this is not a directory we can now return from the function, there is nothing
|
// Chown the directory itself.
|
||||||
// left that we need to do.
|
os.Chown(cleaned, config.Get().System.User.Uid, config.Get().System.User.Gid)
|
||||||
if st, _ := os.Stat(cleaned); !st.IsDir() {
|
|
||||||
return nil
|
files, err := ioutil.ReadDir(cleaned)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this was a directory, begin walking over its contents recursively and ensure that all
|
for _, f := range files {
|
||||||
// of the subfiles and directories get their permissions updated as well.
|
|
||||||
return godirwalk.Walk(cleaned, &godirwalk.Options{
|
|
||||||
Unsorted: true,
|
|
||||||
Callback: func(p string, e *godirwalk.Dirent) error {
|
|
||||||
// Do not attempt to chmod a symlink. Go's os.Chown function will affect the symlink
|
// Do not attempt to chmod a symlink. Go's os.Chown function will affect the symlink
|
||||||
// so if it points to a location outside the data directory the user would be able to
|
// so if it points to a location outside the data directory the user would be able to
|
||||||
// (un)intentionally modify that files permissions.
|
// (un)intentionally modify that files permissions.
|
||||||
if e.IsSymlink() {
|
if f.Mode()&os.ModeSymlink != 0 {
|
||||||
if e.IsDir() {
|
continue
|
||||||
return godirwalk.SkipThis
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p, err := fs.SafeJoin(cleaned, f)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.IsDir() {
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
go func(p string) {
|
||||||
|
defer wg.Done()
|
||||||
|
fs.chownDirectory(p)
|
||||||
|
}(p)
|
||||||
|
} else {
|
||||||
|
os.Chown(p, config.Get().System.User.Uid, config.Get().System.User.Gid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
|
||||||
|
|
||||||
return os.Chown(p, uid, gid)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copies a given file to the same location and appends a suffix to the file to indicate that
|
// Copies a given file to the same location and appends a suffix to the file to indicate that
|
||||||
@@ -586,7 +540,7 @@ func (fs *Filesystem) Copy(p string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if s, err := os.Stat(cleaned); err != nil {
|
if s, err := os.Stat(cleaned); err != nil {
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
} else if s.IsDir() || !s.Mode().IsRegular() {
|
} else if s.IsDir() || !s.Mode().IsRegular() {
|
||||||
// If this is a directory or not a regular file, just throw a not-exist error
|
// If this is a directory or not a regular file, just throw a not-exist error
|
||||||
// since anything calling this function should understand what that means.
|
// since anything calling this function should understand what that means.
|
||||||
@@ -648,8 +602,7 @@ func (fs *Filesystem) Copy(p string) error {
|
|||||||
}
|
}
|
||||||
defer dest.Close()
|
defer dest.Close()
|
||||||
|
|
||||||
buf := make([]byte, 1024*4)
|
if _, err := io.Copy(dest, source); err != nil {
|
||||||
if _, err := io.CopyBuffer(dest, source, buf); err != nil {
|
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -659,7 +612,7 @@ func (fs *Filesystem) Copy(p string) error {
|
|||||||
// Deletes a file or folder from the system. Prevents the user from accidentally
|
// Deletes a file or folder from the system. Prevents the user from accidentally
|
||||||
// (or maliciously) removing their root server data directory.
|
// (or maliciously) removing their root server data directory.
|
||||||
func (fs *Filesystem) Delete(p string) error {
|
func (fs *Filesystem) Delete(p string) error {
|
||||||
// This is one of the few (only?) places in the codebase where we're explicitly not using
|
// This is one of the few (only?) places in the codebase where we're explictly not using
|
||||||
// the SafePath functionality when working with user provided input. If we did, you would
|
// the SafePath functionality when working with user provided input. If we did, you would
|
||||||
// not be able to delete a file that is a symlink pointing to a location outside of the data
|
// not be able to delete a file that is a symlink pointing to a location outside of the data
|
||||||
// directory.
|
// directory.
|
||||||
@@ -678,22 +631,6 @@ func (fs *Filesystem) Delete(p string) error {
|
|||||||
return errors.New("cannot delete root server directory")
|
return errors.New("cannot delete root server directory")
|
||||||
}
|
}
|
||||||
|
|
||||||
if st, err := os.Stat(resolved); err != nil {
|
|
||||||
if !os.IsNotExist(err) {
|
|
||||||
fs.Server.Log().WithField("error", err).WithField("path", resolved).Warn("error while attempting to stat file before deletion")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if !st.IsDir() {
|
|
||||||
atomic.SwapInt64(&fs.disk, -st.Size())
|
|
||||||
} else {
|
|
||||||
go func(st os.FileInfo, resolved string) {
|
|
||||||
if s, err := fs.DirectorySize(resolved); err == nil {
|
|
||||||
atomic.AddInt64(&fs.disk, -s)
|
|
||||||
}
|
|
||||||
}(st, resolved)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return os.RemoveAll(resolved)
|
return os.RemoveAll(resolved)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -725,29 +662,22 @@ func (fs *Filesystem) ListDirectory(p string) ([]*Stat, error) {
|
|||||||
go func(idx int, f os.FileInfo) {
|
go func(idx int, f os.FileInfo) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
var m *mimetype.MIME
|
var m = "inode/directory"
|
||||||
var d = "inode/directory"
|
|
||||||
if !f.IsDir() {
|
if !f.IsDir() {
|
||||||
cleanedp, _ := fs.SafeJoin(cleaned, f)
|
cleanedp, _ := fs.SafeJoin(cleaned, f)
|
||||||
if cleanedp != "" {
|
if cleanedp != "" {
|
||||||
m, _ = mimetype.DetectFile(filepath.Join(cleaned, f.Name()))
|
m, _, _ = mimetype.DetectFile(filepath.Join(cleaned, f.Name()))
|
||||||
} else {
|
} else {
|
||||||
// Just pass this for an unknown type because the file could not safely be resolved within
|
// Just pass this for an unknown type because the file could not safely be resolved within
|
||||||
// the server data path.
|
// the server data path.
|
||||||
d = "application/octet-stream"
|
m = "application/octet-stream"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
st := &Stat{
|
out[idx] = &Stat{
|
||||||
Info: f,
|
Info: f,
|
||||||
Mimetype: d,
|
Mimetype: m,
|
||||||
}
|
}
|
||||||
|
|
||||||
if m != nil {
|
|
||||||
st.Mimetype = m.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
out[idx] = st
|
|
||||||
}(i, file)
|
}(i, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -807,38 +737,26 @@ func (fs *Filesystem) GetIncludedFiles(dir string, ignored []string) (*backup.In
|
|||||||
// files found, and will keep walking deeper and deeper into directories.
|
// files found, and will keep walking deeper and deeper into directories.
|
||||||
inc := new(backup.IncludedFiles)
|
inc := new(backup.IncludedFiles)
|
||||||
|
|
||||||
err = godirwalk.Walk(cleaned, &godirwalk.Options{
|
if err := fs.Walk(cleaned, func(p string, f os.FileInfo, err error) error {
|
||||||
Unsorted: true,
|
|
||||||
Callback: func(p string, e *godirwalk.Dirent) error {
|
|
||||||
sp := p
|
|
||||||
if e.IsSymlink() {
|
|
||||||
sp, err = fs.SafePath(p)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if IsPathResolutionError(err) {
|
return fs.handleWalkerError(err, f)
|
||||||
return godirwalk.SkipThis
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only push files into the result array since archives can't create an empty directory within them.
|
|
||||||
if !e.IsDir() {
|
|
||||||
// Avoid unnecessary parsing if there are no ignored files, nothing will match anyways
|
// Avoid unnecessary parsing if there are no ignored files, nothing will match anyways
|
||||||
// so no reason to call the function.
|
// so no reason to call the function.
|
||||||
if len(ignored) == 0 || !i.MatchesPath(strings.TrimPrefix(sp, fs.Path()+"/")) {
|
if len(ignored) == 0 || !i.MatchesPath(strings.TrimPrefix(p, fs.Path()+"/")) {
|
||||||
inc.Push(sp)
|
inc.Push(&f, p)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We can't just abort if the path is technically ignored. It is possible there is a nested
|
// We can't just abort if the path is technically ignored. It is possible there is a nested
|
||||||
// file or folder that should not be excluded, so in this case we need to just keep going
|
// file or folder that should not be excluded, so in this case we need to just keep going
|
||||||
// until we get to a final state.
|
// until we get to a final state.
|
||||||
return nil
|
return nil
|
||||||
},
|
}); err != nil {
|
||||||
})
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return inc, errors.WithStack(err)
|
return inc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compresses all of the files matching the given paths in the specified directory. This function
|
// Compresses all of the files matching the given paths in the specified directory. This function
|
||||||
@@ -875,38 +793,24 @@ func (fs *Filesystem) CompressFiles(dir string, paths []string) (os.FileInfo, er
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if !f.IsDir() {
|
if f.IsDir() {
|
||||||
inc.Push(p)
|
err := fs.Walk(p, func(s string, info os.FileInfo, err error) error {
|
||||||
} else {
|
|
||||||
err := godirwalk.Walk(p, &godirwalk.Options{
|
|
||||||
Unsorted: true,
|
|
||||||
Callback: func(p string, e *godirwalk.Dirent) error {
|
|
||||||
sp := p
|
|
||||||
if e.IsSymlink() {
|
|
||||||
// Ensure that any symlinks are properly resolved to their final destination. If
|
|
||||||
// that destination is outside the server directory skip over this entire item, otherwise
|
|
||||||
// use the resolved location for the rest of this function.
|
|
||||||
sp, err = fs.SafePath(p)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if IsPathResolutionError(err) {
|
return fs.handleWalkerError(err, info)
|
||||||
return godirwalk.SkipThis
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
if !info.IsDir() {
|
||||||
}
|
inc.Push(&info, s)
|
||||||
}
|
|
||||||
|
|
||||||
if !e.IsDir() {
|
|
||||||
inc.Push(sp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
inc.Push(&f, p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,5 +9,5 @@ import (
|
|||||||
func (s *Stat) CTime() time.Time {
|
func (s *Stat) CTime() time.Time {
|
||||||
st := s.Info.Sys().(*syscall.Stat_t)
|
st := s.Info.Sys().(*syscall.Stat_t)
|
||||||
|
|
||||||
return time.Unix(st.Ctimespec.Sec, st.Ctimespec.Nsec)
|
return time.Unix(int64(st.Ctimespec.Sec), int64(st.Ctimespec.Nsec))
|
||||||
}
|
}
|
||||||
@@ -9,5 +9,5 @@ import (
|
|||||||
func (s *Stat) CTime() time.Time {
|
func (s *Stat) CTime() time.Time {
|
||||||
st := s.Info.Sys().(*syscall.Stat_t)
|
st := s.Info.Sys().(*syscall.Stat_t)
|
||||||
|
|
||||||
return time.Unix(st.Ctim.Sec, st.Ctim.Nsec)
|
return time.Unix(int64(st.Ctim.Sec), int64(st.Ctim.Nsec))
|
||||||
}
|
}
|
||||||
@@ -7,9 +7,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/mholt/archiver/v3"
|
"github.com/mholt/archiver/v3"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,7 +21,7 @@ import (
|
|||||||
func (fs *Filesystem) SpaceAvailableForDecompression(dir string, file string) (bool, error) {
|
func (fs *Filesystem) SpaceAvailableForDecompression(dir string, file string) (bool, error) {
|
||||||
// Don't waste time trying to determine this if we know the server will have the space for
|
// Don't waste time trying to determine this if we know the server will have the space for
|
||||||
// it since there is no limit.
|
// it since there is no limit.
|
||||||
if fs.Server.DiskSpace() <= 0 {
|
if fs.Server.Build().DiskSpace <= 0 {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,19 +30,37 @@ func (fs *Filesystem) SpaceAvailableForDecompression(dir string, file string) (b
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wg := new(sync.WaitGroup)
|
||||||
|
|
||||||
|
var dirSize int64
|
||||||
|
var cErr error
|
||||||
// Get the cached size in a parallel process so that if it is not cached we are not
|
// Get the cached size in a parallel process so that if it is not cached we are not
|
||||||
// waiting an unnecessary amount of time on this call.
|
// waiting an unnecessary amount of time on this call.
|
||||||
dirSize, err := fs.DiskUsage(false)
|
go func() {
|
||||||
|
wg.Add(1)
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
dirSize, cErr = fs.getCachedDiskUsage()
|
||||||
|
}()
|
||||||
|
|
||||||
var size int64
|
var size int64
|
||||||
// Walk over the archive and figure out just how large the final output would be from unarchiving it.
|
// In a seperate thread, walk over the archive and figure out just how large the final
|
||||||
|
// output would be from dearchiving it.
|
||||||
|
go func() {
|
||||||
|
wg.Add(1)
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
// Walk all of the files and calculate the total decompressed size of this archive.
|
||||||
archiver.Walk(source, func(f archiver.File) error {
|
archiver.Walk(source, func(f archiver.File) error {
|
||||||
atomic.AddInt64(&size, f.Size())
|
atomic.AddInt64(&size, f.Size())
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
}()
|
||||||
|
|
||||||
return ((dirSize + size) / 1000.0 / 1000.0) <= fs.Server.DiskSpace(), errors.WithStack(err)
|
wg.Wait()
|
||||||
|
|
||||||
|
return ((dirSize + size) / 1000.0 / 1000.0) <= fs.Server.Build().DiskSpace, cErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decompress a file in a given directory by using the archiver tool to infer the file
|
// Decompress a file in a given directory by using the archiver tool to infer the file
|
||||||
@@ -52,11 +73,6 @@ func (fs *Filesystem) DecompressFile(dir string, file string) error {
|
|||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the file exists basically.
|
|
||||||
if _, err := os.Stat(source); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Walk over all of the files spinning up an additional go-routine for each file we've encountered
|
// Walk over all of the files spinning up an additional go-routine for each file we've encountered
|
||||||
// and then extract that file from the archive and write it to the disk. If any part of this process
|
// and then extract that file from the archive and write it to the disk. If any part of this process
|
||||||
// encounters an error the entire process will be stopped.
|
// encounters an error the entire process will be stopped.
|
||||||
@@ -67,6 +83,13 @@ func (fs *Filesystem) DecompressFile(dir string, file string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return fs.extractFileFromArchive(f)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extracts a single file from the archive and writes it to the disk after verifying that it will end
|
||||||
|
// up in the server data directory.
|
||||||
|
func (fs *Filesystem) extractFileFromArchive(f archiver.File) error {
|
||||||
var name string
|
var name string
|
||||||
|
|
||||||
switch s := f.Sys().(type) {
|
switch s := f.Sys().(type) {
|
||||||
@@ -80,6 +103,28 @@ func (fs *Filesystem) DecompressFile(dir string, file string) error {
|
|||||||
return errors.New(fmt.Sprintf("could not parse underlying data source with type %s", reflect.TypeOf(s).String()))
|
return errors.New(fmt.Sprintf("could not parse underlying data source with type %s", reflect.TypeOf(s).String()))
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.Wrap(fs.Writefile(name, f), "could not extract file from archive")
|
// Guard against a zip-slip attack and prevent writing a file to a destination outside of
|
||||||
})
|
// the server root directory.
|
||||||
|
p, err := fs.SafePath(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the directory structure for this file exists before trying to write the file
|
||||||
|
// to the disk, otherwise we'll have some unexpected fun.
|
||||||
|
if err := os.MkdirAll(strings.TrimSuffix(p, filepath.Base(p)), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the file and truncate it if it already exists.
|
||||||
|
o, err := os.OpenFile(p, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer o.Close()
|
||||||
|
|
||||||
|
_, cerr := io.Copy(o, f)
|
||||||
|
|
||||||
|
return cerr
|
||||||
}
|
}
|
||||||
|
|||||||
141
server/filesystem_walker.go
Normal file
141
server/filesystem_walker.go
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/gammazero/workerpool"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FileWalker struct {
|
||||||
|
*Filesystem
|
||||||
|
}
|
||||||
|
|
||||||
|
type PooledFileWalker struct {
|
||||||
|
wg sync.WaitGroup
|
||||||
|
pool *workerpool.WorkerPool
|
||||||
|
callback filepath.WalkFunc
|
||||||
|
cancel context.CancelFunc
|
||||||
|
|
||||||
|
err error
|
||||||
|
errOnce sync.Once
|
||||||
|
|
||||||
|
Filesystem *Filesystem
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a new walker instance.
|
||||||
|
func (fs *Filesystem) NewWalker() *FileWalker {
|
||||||
|
return &FileWalker{fs}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a new pooled file walker that will concurrently walk over a given directory but limit itself
|
||||||
|
// to a worker pool as to not completely flood out the system or cause a process crash.
|
||||||
|
func newPooledWalker(fs *Filesystem) *PooledFileWalker {
|
||||||
|
return &PooledFileWalker{
|
||||||
|
Filesystem: fs,
|
||||||
|
// Create a worker pool that is the same size as the number of processors available on the
|
||||||
|
// system. Going much higher doesn't provide much of a performance boost, and is only more
|
||||||
|
// likely to lead to resource overloading anyways.
|
||||||
|
pool: workerpool.New(runtime.GOMAXPROCS(0)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process a given path by calling the callback function for all of the files and directories within
|
||||||
|
// the path, and then dropping into any directories that we come across.
|
||||||
|
func (w *PooledFileWalker) process(path string) error {
|
||||||
|
p, err := w.Filesystem.SafePath(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
files, err := ioutil.ReadDir(p)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop over all of the files and directories in the given directory and call the provided
|
||||||
|
// callback function. If we encounter a directory, push that directory onto the worker queue
|
||||||
|
// to be processed.
|
||||||
|
for _, f := range files {
|
||||||
|
sp, err := w.Filesystem.SafeJoin(p, f)
|
||||||
|
if err != nil {
|
||||||
|
// Let the callback function handle what to do if there is a path resolution error because a
|
||||||
|
// dangerous path was resolved. If there is an error returned, return from this entire process
|
||||||
|
// otherwise just skip over this specific file. We don't care if its a file or a directory at
|
||||||
|
// this point since either way we're skipping it, however, still check for the SkipDir since that
|
||||||
|
// would be thrown otherwise.
|
||||||
|
if err = w.callback(sp, f, err); err != nil && err != filepath.SkipDir {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
i, err := os.Stat(sp)
|
||||||
|
// You might end up getting an error about a file or folder not existing if the given path
|
||||||
|
// if it is an invalid symlink. We can safely just skip over these files I believe.
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the user-provided callback for this file or directory. If an error is returned that is
|
||||||
|
// not a SkipDir call, abort the entire process and bubble that error up.
|
||||||
|
if err = w.callback(sp, i, err); err != nil && err != filepath.SkipDir {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is a directory, and we didn't get a SkipDir error, continue through by pushing another
|
||||||
|
// job to the pool to handle it. If we requested a skip, don't do anything just continue on to the
|
||||||
|
// next item.
|
||||||
|
if i.IsDir() && err != filepath.SkipDir {
|
||||||
|
w.push(sp)
|
||||||
|
} else if !i.IsDir() && err == filepath.SkipDir {
|
||||||
|
// Per the spec for the callback, if we get a SkipDir error but it is returned for an item
|
||||||
|
// that is _not_ a directory, abort the remaining operations on the directory.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push a new path into the worker pool and increment the waitgroup so that we do not return too
|
||||||
|
// early and cause panic's as internal directories attempt to submit to the pool.
|
||||||
|
func (w *PooledFileWalker) push(path string) {
|
||||||
|
w.wg.Add(1)
|
||||||
|
w.pool.Submit(func() {
|
||||||
|
defer w.wg.Done()
|
||||||
|
if err := w.process(path); err != nil {
|
||||||
|
w.errOnce.Do(func() {
|
||||||
|
w.err = err
|
||||||
|
if w.cancel != nil {
|
||||||
|
w.cancel()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walks the given directory and executes the callback function for all of the files and directories
|
||||||
|
// that are encountered.
|
||||||
|
func (fs *Filesystem) Walk(dir string, callback filepath.WalkFunc) error {
|
||||||
|
w := newPooledWalker(fs)
|
||||||
|
w.callback = callback
|
||||||
|
|
||||||
|
_, cancel := context.WithCancel(context.Background())
|
||||||
|
w.cancel = cancel
|
||||||
|
|
||||||
|
w.push(dir)
|
||||||
|
|
||||||
|
w.wg.Wait()
|
||||||
|
w.pool.StopWait()
|
||||||
|
|
||||||
|
if w.err != nil {
|
||||||
|
return w.err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -12,20 +12,21 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/pterodactyl/wings/api"
|
"github.com/pterodactyl/wings/api"
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
"github.com/pterodactyl/wings/environment"
|
|
||||||
"golang.org/x/sync/semaphore"
|
"golang.org/x/sync/semaphore"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Executes the installation stack for a server process. Bubbles any errors up to the calling
|
// Executes the installation stack for a server process. Bubbles any errors up to the calling
|
||||||
// function which should handle contacting the panel to notify it of the server state.
|
// function which should handle contacting the panel to notify it of the server state.
|
||||||
//
|
//
|
||||||
// Pass true as the first argument in order to execute a server sync before the process to
|
// Pass true as the first arugment in order to execute a server sync before the process to
|
||||||
// ensure the latest information is used.
|
// ensure the latest information is used.
|
||||||
func (s *Server) Install(sync bool) error {
|
func (s *Server) Install(sync bool) error {
|
||||||
if sync {
|
if sync {
|
||||||
@@ -35,17 +36,7 @@ func (s *Server) Install(sync bool) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
err := s.internalInstall()
|
||||||
if !s.Config().SkipEggScripts {
|
|
||||||
// Send the start event so the Panel can automatically update. We don't send this unless the process
|
|
||||||
// is actually going to run, otherwise all sorts of weird rapid UI behavior happens since there isn't
|
|
||||||
// an actual install process being executed.
|
|
||||||
s.Events().Publish(InstallStartedEvent, "")
|
|
||||||
|
|
||||||
err = s.internalInstall()
|
|
||||||
} else {
|
|
||||||
s.Log().Info("server configured to skip running installation scripts for this egg, not executing process")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Log().Debug("notifying panel of server install state")
|
s.Log().Debug("notifying panel of server install state")
|
||||||
if serr := s.SyncInstallState(err == nil); serr != nil {
|
if serr := s.SyncInstallState(err == nil); serr != nil {
|
||||||
@@ -61,21 +52,13 @@ func (s *Server) Install(sync bool) error {
|
|||||||
l.Warn("failed to notify panel of server install state")
|
l.Warn("failed to notify panel of server install state")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure that the server is marked as offline at this point, otherwise you end up
|
|
||||||
// with a blank value which is a bit confusing.
|
|
||||||
s.SetState(environment.ProcessOfflineState)
|
|
||||||
|
|
||||||
// Push an event to the websocket so we can auto-refresh the information in the panel once
|
|
||||||
// the install is completed.
|
|
||||||
s.Events().Publish(InstallCompletedEvent, "")
|
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reinstalls a server's software by utilizing the install script for the server egg. This
|
// Reinstalls a server's software by utilizing the install script for the server egg. This
|
||||||
// does not touch any existing files for the server, other than what the script modifies.
|
// does not touch any existing files for the server, other than what the script modifies.
|
||||||
func (s *Server) Reinstall() error {
|
func (s *Server) Reinstall() error {
|
||||||
if s.GetState() != environment.ProcessOfflineState {
|
if s.GetState() != ProcessOfflineState {
|
||||||
s.Log().Debug("waiting for server instance to enter a stopped state")
|
s.Log().Debug("waiting for server instance to enter a stopped state")
|
||||||
if err := s.Environment.WaitForStop(10, true); err != nil {
|
if err := s.Environment.WaitForStop(10, true); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -197,7 +180,7 @@ func (ip *InstallationProcess) RemoveContainer() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runs the installation process, this is done as in a background thread. This will configure
|
// Runs the installation process, this is done as a backgrounded thread. This will configure
|
||||||
// the required environment, and then spin up the installation container.
|
// the required environment, and then spin up the installation container.
|
||||||
//
|
//
|
||||||
// Once the container finishes installing the results will be stored in an installation
|
// Once the container finishes installing the results will be stored in an installation
|
||||||
@@ -210,22 +193,23 @@ func (ip *InstallationProcess) Run() error {
|
|||||||
|
|
||||||
// We now have an exclusive lock on this installation process. Ensure that whenever this
|
// We now have an exclusive lock on this installation process. Ensure that whenever this
|
||||||
// process is finished that the semaphore is released so that other processes and be executed
|
// process is finished that the semaphore is released so that other processes and be executed
|
||||||
// without encountering a wait timeout.
|
// without encounting a wait timeout.
|
||||||
defer func() {
|
defer func() {
|
||||||
ip.Server.Log().Debug("releasing installation process lock")
|
ip.Server.Log().Debug("releasing installation process lock")
|
||||||
ip.Server.installer.sem.Release(1)
|
ip.Server.installer.sem.Release(1)
|
||||||
ip.Server.installer.cancel = nil
|
ip.Server.installer.cancel = nil
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err := ip.BeforeExecute(); err != nil {
|
installPath, err := ip.BeforeExecute()
|
||||||
return errors.WithStack(err)
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cid, err := ip.Execute()
|
cid, err := ip.Execute(installPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ip.RemoveContainer()
|
ip.RemoveContainer()
|
||||||
|
|
||||||
return errors.WithStack(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this step fails, log a warning but don't exit out of the process. This is completely
|
// If this step fails, log a warning but don't exit out of the process. This is completely
|
||||||
@@ -237,23 +221,23 @@ func (ip *InstallationProcess) Run() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the location of the temporary data for the installation process.
|
|
||||||
func (ip *InstallationProcess) tempDir() string {
|
|
||||||
return filepath.Join(os.TempDir(), "pterodactyl/", ip.Server.Id())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Writes the installation script to a temporary file on the host machine so that it
|
// Writes the installation script to a temporary file on the host machine so that it
|
||||||
// can be properly mounted into the installation container and then executed.
|
// can be properly mounted into the installation container and then executed.
|
||||||
func (ip *InstallationProcess) writeScriptToDisk() error {
|
func (ip *InstallationProcess) writeScriptToDisk() (string, error) {
|
||||||
// Make sure the temp directory root exists before trying to make a directory within it. The
|
// Make sure the temp directory root exists before trying to make a directory within it. The
|
||||||
// ioutil.TempDir call expects this base to exist, it won't create it for you.
|
// ioutil.TempDir call expects this base to exist, it won't create it for you.
|
||||||
if err := os.MkdirAll(ip.tempDir(), 0700); err != nil {
|
if err := os.MkdirAll(path.Join(os.TempDir(), "pterodactyl/"), 0700); err != nil {
|
||||||
return errors.Wrap(err, "could not create temporary directory for install process")
|
return "", errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := os.OpenFile(filepath.Join(ip.tempDir(), "install.sh"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
d, err := ioutil.TempDir("", "pterodactyl/")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to write server installation script to disk before mount")
|
return "", errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.OpenFile(filepath.Join(d, "install.sh"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.WithStack(err)
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
@@ -265,12 +249,12 @@ func (ip *InstallationProcess) writeScriptToDisk() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
if err := scanner.Err(); err != nil {
|
||||||
return errors.WithStack(err)
|
return "", errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Flush()
|
w.Flush()
|
||||||
|
|
||||||
return nil
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pulls the docker image to be used for the installation container.
|
// Pulls the docker image to be used for the installation container.
|
||||||
@@ -296,14 +280,33 @@ func (ip *InstallationProcess) pullInstallationImage() error {
|
|||||||
// Runs before the container is executed. This pulls down the required docker container image
|
// Runs before the container is executed. This pulls down the required docker container image
|
||||||
// as well as writes the installation script to the disk. This process is executed in an async
|
// as well as writes the installation script to the disk. This process is executed in an async
|
||||||
// manner, if either one fails the error is returned.
|
// manner, if either one fails the error is returned.
|
||||||
func (ip *InstallationProcess) BeforeExecute() error {
|
func (ip *InstallationProcess) BeforeExecute() (string, error) {
|
||||||
if err := ip.writeScriptToDisk(); err != nil {
|
wg := sync.WaitGroup{}
|
||||||
return errors.Wrap(err, "failed to write installation script to disk")
|
wg.Add(3)
|
||||||
|
|
||||||
|
var e []error
|
||||||
|
var fileName string
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
name, err := ip.writeScriptToDisk()
|
||||||
|
if err != nil {
|
||||||
|
e = append(e, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileName = name
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
if err := ip.pullInstallationImage(); err != nil {
|
if err := ip.pullInstallationImage(); err != nil {
|
||||||
return errors.Wrap(err, "failed to pull updated installation container image for server")
|
e = append(e, err)
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
opts := types.ContainerRemoveOptions{
|
opts := types.ContainerRemoveOptions{
|
||||||
RemoveVolumes: true,
|
RemoveVolumes: true,
|
||||||
@@ -312,11 +315,20 @@ func (ip *InstallationProcess) BeforeExecute() error {
|
|||||||
|
|
||||||
if err := ip.client.ContainerRemove(ip.context, ip.Server.Id()+"_installer", opts); err != nil {
|
if err := ip.client.ContainerRemove(ip.context, ip.Server.Id()+"_installer", opts); err != nil {
|
||||||
if !client.IsErrNotFound(err) {
|
if !client.IsErrNotFound(err) {
|
||||||
return errors.Wrap(err, "failed to remove existing install container for server")
|
e = append(e, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// Maybe a better way to handle this, but if there is at least one error
|
||||||
|
// just bail out of the process now.
|
||||||
|
if len(e) > 0 {
|
||||||
|
return "", errors.WithStack(e[0])
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return fileName, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the log path for the installation process.
|
// Returns the log path for the installation process.
|
||||||
@@ -357,7 +369,7 @@ func (ip *InstallationProcess) AfterExecute(containerId string) error {
|
|||||||
|
|
|
|
||||||
| Details
|
| Details
|
||||||
| ------------------------------
|
| ------------------------------
|
||||||
Server UUID: {{.Server.Id}}
|
Server UUID: {{.Server.Id()}}
|
||||||
Container Image: {{.Script.ContainerImage}}
|
Container Image: {{.Script.ContainerImage}}
|
||||||
Container Entrypoint: {{.Script.Entrypoint}}
|
Container Entrypoint: {{.Script.Entrypoint}}
|
||||||
|
|
||||||
@@ -387,7 +399,7 @@ func (ip *InstallationProcess) AfterExecute(containerId string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Executes the installation process inside a specially created docker container.
|
// Executes the installation process inside a specially created docker container.
|
||||||
func (ip *InstallationProcess) Execute() (string, error) {
|
func (ip *InstallationProcess) Execute(installPath string) (string, error) {
|
||||||
conf := &container.Config{
|
conf := &container.Config{
|
||||||
Hostname: "installer",
|
Hostname: "installer",
|
||||||
AttachStdout: true,
|
AttachStdout: true,
|
||||||
@@ -404,7 +416,6 @@ func (ip *InstallationProcess) Execute() (string, error) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpfsSize := strconv.Itoa(int(config.Get().Docker.TmpfsSize))
|
|
||||||
hostConf := &container.HostConfig{
|
hostConf := &container.HostConfig{
|
||||||
Mounts: []mount.Mount{
|
Mounts: []mount.Mount{
|
||||||
{
|
{
|
||||||
@@ -415,13 +426,13 @@ func (ip *InstallationProcess) Execute() (string, error) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Target: "/mnt/install",
|
Target: "/mnt/install",
|
||||||
Source: ip.tempDir(),
|
Source: installPath,
|
||||||
Type: mount.TypeBind,
|
Type: mount.TypeBind,
|
||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Tmpfs: map[string]string{
|
Tmpfs: map[string]string{
|
||||||
"/tmp": "rw,exec,nosuid,size=" + tmpfsSize + "M",
|
"/tmp": "rw,exec,nosuid,size=50M",
|
||||||
},
|
},
|
||||||
DNS: config.Get().Docker.Network.Dns,
|
DNS: config.Get().Docker.Network.Dns,
|
||||||
LogConfig: container.LogConfig{
|
LogConfig: container.LogConfig{
|
||||||
@@ -436,16 +447,7 @@ func (ip *InstallationProcess) Execute() (string, error) {
|
|||||||
NetworkMode: container.NetworkMode(config.Get().Docker.Network.Mode),
|
NetworkMode: container.NetworkMode(config.Get().Docker.Network.Mode),
|
||||||
}
|
}
|
||||||
|
|
||||||
ip.Server.Log().WithField("install_script", ip.tempDir()+"/install.sh").Info("creating install container for server process")
|
ip.Server.Log().WithField("install_script", installPath+"/install.sh").Info("creating install container for server process")
|
||||||
// Remove the temporary directory when the installation process finishes for this server container.
|
|
||||||
defer func() {
|
|
||||||
if err := os.RemoveAll(ip.tempDir()); err != nil {
|
|
||||||
if !os.IsNotExist(err) {
|
|
||||||
ip.Server.Log().WithField("error", err).Warn("failed to remove temporary data directory after install process")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
r, err := ip.client.ContainerCreate(ip.context, conf, hostConf, nil, ip.Server.Id()+"_installer")
|
r, err := ip.client.ContainerCreate(ip.context, conf, hostConf, nil, ip.Server.Id()+"_installer")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.WithStack(err)
|
return "", errors.WithStack(err)
|
||||||
@@ -464,13 +466,13 @@ func (ip *InstallationProcess) Execute() (string, error) {
|
|||||||
ip.Server.Events().Publish(DaemonMessageEvent, "Installation process completed.")
|
ip.Server.Events().Publish(DaemonMessageEvent, "Installation process completed.")
|
||||||
}(r.ID)
|
}(r.ID)
|
||||||
|
|
||||||
sChan, eChan := ip.client.ContainerWait(ip.context, r.ID, container.WaitConditionNotRunning)
|
sChann, eChann := ip.client.ContainerWait(ip.context, r.ID, container.WaitConditionNotRunning)
|
||||||
select {
|
select {
|
||||||
case err := <-eChan:
|
case err := <-eChann:
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.WithStack(err)
|
return "", errors.WithStack(err)
|
||||||
}
|
}
|
||||||
case <-sChan:
|
case <-sChann:
|
||||||
}
|
}
|
||||||
|
|
||||||
return r.ID, nil
|
return r.ID, nil
|
||||||
|
|||||||
@@ -1,107 +1,50 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/pterodactyl/wings/api"
|
"github.com/pterodactyl/wings/api"
|
||||||
"github.com/pterodactyl/wings/environment"
|
"strings"
|
||||||
"github.com/pterodactyl/wings/events"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Adds all of the internal event listeners we want to use for a server.
|
// Adds all of the internal event listeners we want to use for a server.
|
||||||
func (s *Server) StartEventListeners() {
|
func (s *Server) AddEventListeners() {
|
||||||
console := make(chan events.Event)
|
consoleChannel := make(chan Event)
|
||||||
state := make(chan events.Event)
|
s.Events().Subscribe(ConsoleOutputEvent, consoleChannel)
|
||||||
stats := make(chan events.Event)
|
|
||||||
|
|
||||||
s.Environment.Events().Subscribe(environment.ConsoleOutputEvent, console)
|
go func() {
|
||||||
s.Environment.Events().Subscribe(environment.StateChangeEvent, state)
|
for {
|
||||||
s.Environment.Events().Subscribe(environment.ResourceEvent, stats)
|
select {
|
||||||
|
case data := <-consoleChannel:
|
||||||
go func(console chan events.Event) {
|
|
||||||
for data := range console {
|
|
||||||
// Immediately emit this event back over the server event stream since it is
|
|
||||||
// being called from the environment event stream and things probably aren't
|
|
||||||
// listening to that event.
|
|
||||||
s.Events().Publish(ConsoleOutputEvent, data.Data)
|
|
||||||
|
|
||||||
// Also pass the data along to the console output channel.
|
|
||||||
s.onConsoleOutput(data.Data)
|
s.onConsoleOutput(data.Data)
|
||||||
}
|
}
|
||||||
}(console)
|
|
||||||
|
|
||||||
go func(state chan events.Event) {
|
|
||||||
for data := range state {
|
|
||||||
s.SetState(data.Data)
|
|
||||||
}
|
}
|
||||||
}(state)
|
}()
|
||||||
|
|
||||||
go func(stats chan events.Event) {
|
|
||||||
for data := range stats {
|
|
||||||
st := new(environment.Stats)
|
|
||||||
if err := json.Unmarshal([]byte(data.Data), st); err != nil {
|
|
||||||
s.Log().WithField("error", errors.WithStack(err)).Warn("failed to unmarshal server environment stats")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the server resource tracking object with the resources we got here.
|
|
||||||
s.resources.mu.Lock()
|
|
||||||
s.resources.Stats = *st
|
|
||||||
s.resources.mu.Unlock()
|
|
||||||
|
|
||||||
s.Filesystem.HasSpaceAvailable(true)
|
|
||||||
|
|
||||||
s.emitProcUsage()
|
|
||||||
}
|
|
||||||
}(stats)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var stripAnsiRegex = regexp.MustCompile("[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))")
|
|
||||||
|
|
||||||
// Custom listener for console output events that will check if the given line
|
// Custom listener for console output events that will check if the given line
|
||||||
// of output matches one that should mark the server as started or not.
|
// of output matches one that should mark the server as started or not.
|
||||||
func (s *Server) onConsoleOutput(data string) {
|
func (s *Server) onConsoleOutput(data string) {
|
||||||
// Get the server's process configuration.
|
|
||||||
processConfiguration := s.ProcessConfiguration()
|
|
||||||
|
|
||||||
// Check if the server is currently starting.
|
|
||||||
if s.GetState() == environment.ProcessStartingState {
|
|
||||||
// Check if we should strip ansi color codes.
|
|
||||||
if processConfiguration.Startup.StripAnsi {
|
|
||||||
// Strip ansi color codes from the data string.
|
|
||||||
data = stripAnsiRegex.ReplaceAllString(data, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterate over all the done lines.
|
|
||||||
for _, l := range processConfiguration.Startup.Done {
|
|
||||||
if !l.Matches(data) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Log().WithFields(log.Fields{
|
|
||||||
"match": l.String(),
|
|
||||||
"against": strconv.QuoteToASCII(data),
|
|
||||||
}).Debug("detected server in running state based on console line output")
|
|
||||||
|
|
||||||
// If the specific line of output is one that would mark the server as started,
|
// If the specific line of output is one that would mark the server as started,
|
||||||
// set the server to that state. Only do this if the server is not currently stopped
|
// set the server to that state. Only do this if the server is not currently stopped
|
||||||
// or stopping.
|
// or stopping.
|
||||||
_ = s.SetState(environment.ProcessRunningState)
|
match := s.ProcessConfiguration().Startup.Done
|
||||||
break
|
|
||||||
}
|
if s.GetState() == ProcessStartingState && strings.Contains(data, match) {
|
||||||
|
s.Log().WithFields(log.Fields{
|
||||||
|
"match": match,
|
||||||
|
"against": data,
|
||||||
|
}).Debug("detected server in running state based on console line output")
|
||||||
|
|
||||||
|
s.SetState(ProcessRunningState)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the command sent to the server is one that should stop the server we will need to
|
// If the command sent to the server is one that should stop the server we will need to
|
||||||
// set the server to be in a stopping state, otherwise crash detection will kick in and
|
// set the server to be in a stopping state, otherwise crash detection will kick in and
|
||||||
// cause the server to unexpectedly restart on the user.
|
// cause the server to unexpectedly restart on the user.
|
||||||
if s.IsRunning() {
|
if s.IsRunning() {
|
||||||
stop := processConfiguration.Stop
|
stop := s.ProcessConfiguration().Stop
|
||||||
|
|
||||||
if stop.Type == api.ProcessStopCommand && data == stop.Value {
|
if stop.Type == api.ProcessStopCommand && data == stop.Value {
|
||||||
_ = s.SetState(environment.ProcessOfflineState)
|
s.SetState(ProcessStoppingState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/creasty/defaults"
|
"github.com/creasty/defaults"
|
||||||
"github.com/gammazero/workerpool"
|
"github.com/patrickmn/go-cache"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/pterodactyl/wings/api"
|
"github.com/pterodactyl/wings/api"
|
||||||
"github.com/pterodactyl/wings/environment"
|
"github.com/remeh/sizedwaitgroup"
|
||||||
"github.com/pterodactyl/wings/environment/docker"
|
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,7 +23,16 @@ func LoadDirectory() error {
|
|||||||
return errors.New("cannot call LoadDirectory with a non-nil collection")
|
return errors.New("cannot call LoadDirectory with a non-nil collection")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("fetching list of servers from API")
|
// We could theoretically use a standard wait group here, however doing
|
||||||
|
// that introduces the potential to crash the program due to too many
|
||||||
|
// open files. This wouldn't happen on a small setup, but once the daemon is
|
||||||
|
// handling many servers you run that risk.
|
||||||
|
//
|
||||||
|
// For now just process 10 files at a time, that should be plenty fast to
|
||||||
|
// read and parse the YAML. We should probably make this configurable down
|
||||||
|
// the road to help big instances scale better.
|
||||||
|
wg := sizedwaitgroup.New(10)
|
||||||
|
|
||||||
configs, rerr, err := api.NewRequester().GetAllServerConfigurations()
|
configs, rerr, err := api.NewRequester().GetAllServerConfigurations()
|
||||||
if err != nil || rerr != nil {
|
if err != nil || rerr != nil {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -40,19 +45,17 @@ func LoadDirectory() error {
|
|||||||
log.Debug("retrieving cached server states from disk")
|
log.Debug("retrieving cached server states from disk")
|
||||||
states, err := getServerStates()
|
states, err := getServerStates()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithField("error", errors.WithStack(err)).Error("failed to retrieve locally cached server states from disk, assuming all servers in offline state")
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
start := time.Now()
|
log.WithField("total_configs", len(configs)).Debug("looping over received configurations from API")
|
||||||
log.WithField("total_configs", len(configs)).Info("processing servers returned by the API")
|
|
||||||
|
|
||||||
pool := workerpool.New(runtime.NumCPU())
|
|
||||||
for uuid, data := range configs {
|
for uuid, data := range configs {
|
||||||
uuid := uuid
|
wg.Add()
|
||||||
data := data
|
|
||||||
|
|
||||||
pool.Submit(func() {
|
go func(uuid string, data *api.ServerConfigurationResponse) {
|
||||||
log.WithField("server", uuid).Info("creating new server object from API response")
|
defer wg.Done()
|
||||||
|
|
||||||
|
log.WithField("uuid", uuid).Debug("creating server object from configuration")
|
||||||
s, err := FromConfiguration(data)
|
s, err := FromConfiguration(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithField("server", uuid).WithField("error", err).Error("failed to load server, skipping...")
|
log.WithField("server", uuid).WithField("error", err).Error("failed to load server, skipping...")
|
||||||
@@ -60,20 +63,17 @@ func LoadDirectory() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if state, exists := states[s.Id()]; exists {
|
if state, exists := states[s.Id()]; exists {
|
||||||
s.Log().WithField("state", state).Debug("found existing server state in cache file; re-instantiating server state")
|
|
||||||
s.SetState(state)
|
s.SetState(state)
|
||||||
|
s.Log().WithField("state", s.GetState()).Debug("loaded server state from cache file")
|
||||||
}
|
}
|
||||||
|
|
||||||
servers.Add(s)
|
servers.Add(s)
|
||||||
})
|
}(uuid, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait until we've processed all of the configuration files in the directory
|
// Wait until we've processed all of the configuration files in the directory
|
||||||
// before continuing.
|
// before continuing.
|
||||||
pool.StopWait()
|
wg.Wait()
|
||||||
|
|
||||||
diff := time.Now().Sub(start)
|
|
||||||
log.WithField("duration", fmt.Sprintf("%s", diff)).Info("finished processing server configurations")
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -84,41 +84,31 @@ func LoadDirectory() error {
|
|||||||
func FromConfiguration(data *api.ServerConfigurationResponse) (*Server, error) {
|
func FromConfiguration(data *api.ServerConfigurationResponse) (*Server, error) {
|
||||||
cfg := Configuration{}
|
cfg := Configuration{}
|
||||||
if err := defaults.Set(&cfg); err != nil {
|
if err := defaults.Set(&cfg); err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to set struct defaults for server configuration")
|
|
||||||
}
|
|
||||||
|
|
||||||
s := new(Server)
|
|
||||||
if err := defaults.Set(s); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to set struct defaults for server")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.cfg = cfg
|
|
||||||
if err := s.UpdateDataStructure(data.Settings); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Archiver = Archiver{Server: s}
|
s := new(Server)
|
||||||
s.Filesystem = Filesystem{Server: s}
|
s.cfg = cfg
|
||||||
|
|
||||||
|
if err := s.UpdateDataStructure(data.Settings, false); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.AddEventListeners()
|
||||||
|
|
||||||
// Right now we only support a Docker based environment, so I'm going to hard code
|
// 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
|
// this logic in. When we're ready to support other environment we'll need to make
|
||||||
// some modifications here obviously.
|
// some modifications here obviously.
|
||||||
settings := environment.Settings{
|
if err := NewDockerEnvironment(s); err != nil {
|
||||||
Mounts: s.Mounts(),
|
|
||||||
Allocations: s.cfg.Allocations,
|
|
||||||
Limits: s.cfg.Build,
|
|
||||||
}
|
|
||||||
|
|
||||||
envCfg := environment.NewConfiguration(settings, s.GetEnvironmentVariables())
|
|
||||||
meta := docker.Metadata{
|
|
||||||
Image: s.Config().Container.Image,
|
|
||||||
}
|
|
||||||
|
|
||||||
if env, err := docker.New(s.Id(), &meta, envCfg); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
}
|
||||||
s.Environment = env
|
|
||||||
go s.StartEventListeners()
|
s.cache = cache.New(time.Minute*10, time.Minute*15)
|
||||||
|
s.Archiver = Archiver{
|
||||||
|
Server: s,
|
||||||
|
}
|
||||||
|
s.Filesystem = Filesystem{
|
||||||
|
Server: s,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forces the configuration to be synced with the panel.
|
// Forces the configuration to be synced with the panel.
|
||||||
@@ -126,10 +116,5 @@ func FromConfiguration(data *api.ServerConfigurationResponse) (*Server, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the server's data directory exists, force disk usage calculation.
|
|
||||||
if _, err := os.Stat(s.Filesystem.Path()); err == nil {
|
|
||||||
s.Filesystem.HasSpaceAvailable(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|||||||
8
server/mount.go
Normal file
8
server/mount.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
// Mount represents a Server Mount.
|
||||||
|
type Mount struct {
|
||||||
|
Target string `json:"target"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
ReadOnly bool `json:"read_only"`
|
||||||
|
}
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/apex/log"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/pterodactyl/wings/config"
|
|
||||||
"github.com/pterodactyl/wings/environment"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// To avoid confusion when working with mounts, assume that a server.Mount has not been properly
|
|
||||||
// cleaned up and had the paths set. An environment.Mount should only be returned with valid paths
|
|
||||||
// that have been checked.
|
|
||||||
type Mount environment.Mount
|
|
||||||
|
|
||||||
// Returns the default container mounts for the server instance. This includes the data directory
|
|
||||||
// for the server as well as any timezone related files if they exist on the host system so that
|
|
||||||
// servers running within the container will use the correct time.
|
|
||||||
func (s *Server) Mounts() []environment.Mount {
|
|
||||||
var m []environment.Mount
|
|
||||||
|
|
||||||
m = append(m, environment.Mount{
|
|
||||||
Default: true,
|
|
||||||
Target: "/home/container",
|
|
||||||
Source: s.Filesystem.Path(),
|
|
||||||
ReadOnly: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Try to mount in /etc/localtime and /etc/timezone if they exist on the host system.
|
|
||||||
if _, err := os.Stat("/etc/localtime"); err != nil {
|
|
||||||
if !os.IsNotExist(err) {
|
|
||||||
log.WithField("error", errors.WithStack(err)).Warn("failed to stat /etc/localtime due to an error")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
m = append(m, environment.Mount{
|
|
||||||
Target: "/etc/localtime",
|
|
||||||
Source: "/etc/localtime",
|
|
||||||
ReadOnly: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := os.Stat("/etc/timezone"); err != nil {
|
|
||||||
if !os.IsNotExist(err) {
|
|
||||||
log.WithField("error", errors.WithStack(err)).Warn("failed to stat /etc/timezone due to an error")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
m = append(m, environment.Mount{
|
|
||||||
Target: "/etc/timezone",
|
|
||||||
Source: "/etc/timezone",
|
|
||||||
ReadOnly: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also include any of this server's custom mounts when returning them.
|
|
||||||
return append(m, s.customMounts()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the custom mounts for a given server after verifying that they are within a list of
|
|
||||||
// allowed mount points for the node.
|
|
||||||
func (s *Server) customMounts() []environment.Mount {
|
|
||||||
var mounts []environment.Mount
|
|
||||||
|
|
||||||
// TODO: probably need to handle things trying to mount directories that do not exist.
|
|
||||||
for _, m := range s.Config().Mounts {
|
|
||||||
source := filepath.Clean(m.Source)
|
|
||||||
target := filepath.Clean(m.Target)
|
|
||||||
|
|
||||||
logger := s.Log().WithFields(log.Fields{
|
|
||||||
"source_path": source,
|
|
||||||
"target_path": target,
|
|
||||||
"read_only": m.ReadOnly,
|
|
||||||
})
|
|
||||||
|
|
||||||
mounted := false
|
|
||||||
for _, allowed := range config.Get().AllowedMounts {
|
|
||||||
if !strings.HasPrefix(source, allowed) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
mounted = true
|
|
||||||
mounts = append(mounts, environment.Mount{
|
|
||||||
Source: source,
|
|
||||||
Target: target,
|
|
||||||
ReadOnly: m.ReadOnly,
|
|
||||||
})
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if !mounted {
|
|
||||||
logger.Warn("skipping custom server mount, not in list of allowed mount points")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mounts
|
|
||||||
}
|
|
||||||
174
server/power.go
174
server/power.go
@@ -1,172 +1,12 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
type PowerAction struct {
|
||||||
"context"
|
Action string `json:"action"`
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/pterodactyl/wings/config"
|
|
||||||
"golang.org/x/sync/semaphore"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PowerAction string
|
|
||||||
|
|
||||||
// The power actions that can be performed for a given server. This taps into the given server
|
|
||||||
// environment and performs them in a way that prevents a race condition from occurring. For
|
|
||||||
// example, sending two "start" actions back to back will not process the second action until
|
|
||||||
// the first action has been completed.
|
|
||||||
//
|
|
||||||
// This utilizes a workerpool with a limit of one worker so that all of the actions execute
|
|
||||||
// in a sync manner.
|
|
||||||
const (
|
|
||||||
PowerActionStart = "start"
|
|
||||||
PowerActionStop = "stop"
|
|
||||||
PowerActionRestart = "restart"
|
|
||||||
PowerActionTerminate = "kill"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Checks if the power action being received is valid.
|
|
||||||
func (pa PowerAction) IsValid() bool {
|
|
||||||
return pa == PowerActionStart ||
|
|
||||||
pa == PowerActionStop ||
|
|
||||||
pa == PowerActionTerminate ||
|
|
||||||
pa == PowerActionRestart
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pa PowerAction) IsStart() bool {
|
func (pr *PowerAction) IsValid() bool {
|
||||||
return pa == PowerActionStart || pa == PowerActionRestart
|
return pr.Action == "start" ||
|
||||||
}
|
pr.Action == "stop" ||
|
||||||
|
pr.Action == "kill" ||
|
||||||
// Helper function that can receive a power action and then process the actions that need
|
pr.Action == "restart"
|
||||||
// to occur for it. This guards against someone calling Start() twice at the same time, or
|
|
||||||
// trying to restart while another restart process is currently running.
|
|
||||||
//
|
|
||||||
// However, the code design for the daemon does depend on the user correctly calling this
|
|
||||||
// function rather than making direct calls to the start/stop/restart functions on the
|
|
||||||
// environment struct.
|
|
||||||
func (s *Server) HandlePowerAction(action PowerAction, waitSeconds ...int) error {
|
|
||||||
if s.powerLock == nil {
|
|
||||||
s.powerLock = semaphore.NewWeighted(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only attempt to acquire a lock on the process if this is not a termination event. We want to
|
|
||||||
// just allow those events to pass right through for good reason. If a server is currently trying
|
|
||||||
// to process a power action but has gotten stuck you still should be able to pass through the
|
|
||||||
// terminate event. The good news here is that doing that oftentimes will get the stuck process to
|
|
||||||
// move again, and naturally continue through the process.
|
|
||||||
if action != PowerActionTerminate {
|
|
||||||
// Determines if we should wait for the lock or not. If a value greater than 0 is passed
|
|
||||||
// into this function we will wait that long for a lock to be acquired.
|
|
||||||
if len(waitSeconds) > 0 && waitSeconds[0] != 0 {
|
|
||||||
ctx, _ := context.WithTimeout(context.Background(), time.Second*time.Duration(waitSeconds[0]))
|
|
||||||
// Attempt to acquire a lock on the power action lock for up to 30 seconds. If more
|
|
||||||
// time than that passes an error will be propagated back up the chain and this
|
|
||||||
// request will be aborted.
|
|
||||||
if err := s.powerLock.Acquire(ctx, 1); err != nil {
|
|
||||||
return errors.Wrap(err, "could not acquire lock on power state")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If no wait duration was provided we will attempt to immediately acquire the lock
|
|
||||||
// and bail out with a context deadline error if it is not acquired immediately.
|
|
||||||
if ok := s.powerLock.TryAcquire(1); !ok {
|
|
||||||
return errors.Wrap(context.DeadlineExceeded, "could not acquire lock on power state")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Release the lock once the process being requested has finished executing.
|
|
||||||
defer s.powerLock.Release(1)
|
|
||||||
} else {
|
|
||||||
// Still try to acquire the lock if terminating and it is available, just so that other power
|
|
||||||
// actions are blocked until it has completed. However, if it is unavailable we won't stop
|
|
||||||
// the entire process.
|
|
||||||
if ok := s.powerLock.TryAcquire(1); ok {
|
|
||||||
// If we managed to acquire the lock be sure to released it once this process is completed.
|
|
||||||
defer s.powerLock.Release(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch action {
|
|
||||||
case PowerActionStart:
|
|
||||||
// Run the pre-boot logic for the server before processing the environment start.
|
|
||||||
if err := s.onBeforeStart(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.Environment.Start()
|
|
||||||
case PowerActionStop:
|
|
||||||
// We're specifically waiting for the process to be stopped here, otherwise the lock is released
|
|
||||||
// too soon, and you can rack up all sorts of issues.
|
|
||||||
return s.Environment.WaitForStop(10*60, true)
|
|
||||||
case PowerActionRestart:
|
|
||||||
if err := s.Environment.WaitForStop(10*60, true); err != nil {
|
|
||||||
// Even timeout errors should be bubbled back up the stack. If the process didn't stop
|
|
||||||
// nicely, but the terminate argument was passed then the server is stopped without an
|
|
||||||
// error being returned.
|
|
||||||
//
|
|
||||||
// However, if terminate is not passed you'll get a context deadline error. We could
|
|
||||||
// probably handle that nicely here, but I'd rather just pass it back up the stack for now.
|
|
||||||
// Either way, any type of error indicates we should not attempt to start the server back
|
|
||||||
// up.
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now actually try to start the process by executing the normal pre-boot logic.
|
|
||||||
if err := s.onBeforeStart(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.Environment.Start()
|
|
||||||
case PowerActionTerminate:
|
|
||||||
return s.Environment.Terminate(os.Kill)
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.New("attempting to handle unknown power action")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute a few functions before actually calling the environment start commands. This ensures
|
|
||||||
// that everything is ready to go for environment booting, and that the server can even be started.
|
|
||||||
func (s *Server) onBeforeStart() error {
|
|
||||||
s.Log().Info("syncing server configuration with panel")
|
|
||||||
if err := s.Sync(); err != nil {
|
|
||||||
return errors.Wrap(err, "unable to sync server data from Panel instance")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disallow start & restart if the server is suspended. Do this check after performing a sync
|
|
||||||
// action with the Panel to ensure that we have the most up-to-date information for that server.
|
|
||||||
if s.IsSuspended() {
|
|
||||||
return new(suspendedError)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure we sync the server information with the environment so that any new environment variables
|
|
||||||
// and process resource limits are correctly applied.
|
|
||||||
s.SyncWithEnvironment()
|
|
||||||
|
|
||||||
// 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 !s.Filesystem.HasSpaceAvailable(false) {
|
|
||||||
return errors.New("cannot start server, not enough disk space available")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the configuration files defined for the server before beginning the boot process.
|
|
||||||
// This process executes a bunch of parallel updates, so we just block until that process
|
|
||||||
// is complete. Any errors as a result of this will just be bubbled out in the logger,
|
|
||||||
// we don't need to actively do anything about it at this point, worst comes to worst the
|
|
||||||
// server starts in a weird state and the user can manually adjust.
|
|
||||||
s.PublishConsoleOutputFromDaemon("Updating process configuration files...")
|
|
||||||
s.UpdateConfigurationFiles()
|
|
||||||
|
|
||||||
if config.Get().System.CheckPermissionsOnBoot {
|
|
||||||
s.PublishConsoleOutputFromDaemon("Ensuring file permissions are set correctly, this could take a few seconds...")
|
|
||||||
// Ensure all of the server file permissions are set correctly before booting the process.
|
|
||||||
if err := s.Filesystem.Chown("/"); err != nil {
|
|
||||||
return errors.Wrap(err, "failed to chown root server directory during pre-boot process")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
10
server/process.go
Normal file
10
server/process.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import "github.com/pterodactyl/wings/api"
|
||||||
|
|
||||||
|
func (s *Server) ProcessConfiguration() *api.ProcessConfiguration {
|
||||||
|
s.RLock()
|
||||||
|
defer s.RUnlock()
|
||||||
|
|
||||||
|
return s.procConfig
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/pterodactyl/wings/environment"
|
"math"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Defines the current resource usage for a given server instance. If a server is offline you
|
// Defines the current resource usage for a given server instance. If a server is offline you
|
||||||
@@ -12,15 +13,33 @@ import (
|
|||||||
type ResourceUsage struct {
|
type ResourceUsage struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
|
|
||||||
// Embed the current environment stats into this server specific resource usage struct.
|
|
||||||
environment.Stats
|
|
||||||
|
|
||||||
// The current server status.
|
// The current server status.
|
||||||
State string `json:"state" default:"offline"`
|
State string `json:"state" default:"offline"`
|
||||||
|
|
||||||
|
// The total amount of memory, in bytes, that this server instance is consuming. This is
|
||||||
|
// calculated slightly differently than just using the raw Memory field that the stats
|
||||||
|
// return from the container, so please check the code setting this value for how that
|
||||||
|
// is calculated.
|
||||||
|
Memory uint64 `json:"memory_bytes"`
|
||||||
|
|
||||||
|
// The total amount of memory this container or resource can use. Inside Docker this is
|
||||||
|
// going to be higher than you'd expect because we're automatically allocating overhead
|
||||||
|
// abilities for the container, so its not going to be a perfect match.
|
||||||
|
MemoryLimit uint64 `json:"memory_limit_bytes"`
|
||||||
|
|
||||||
|
// The absolute CPU usage is the amount of CPU used in relation to the entire system and
|
||||||
|
// does not take into account any limits on the server process itself.
|
||||||
|
CpuAbsolute float64 `json:"cpu_absolute"`
|
||||||
|
|
||||||
// The current disk space being used by the server. This is cached to prevent slow lookup
|
// The current disk space being used by the server. This is cached to prevent slow lookup
|
||||||
// issues on frequent refreshes.
|
// issues on frequent refreshes.
|
||||||
Disk int64 `json:"disk_bytes"`
|
Disk int64 `json:"disk_bytes"`
|
||||||
|
|
||||||
|
// Current network transmit in & out for a container.
|
||||||
|
Network struct {
|
||||||
|
RxBytes uint64 `json:"rx_bytes"`
|
||||||
|
TxBytes uint64 `json:"tx_bytes"`
|
||||||
|
} `json:"network"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the resource usage stats for the server instance. If the server is not running, only the
|
// Returns the resource usage stats for the server instance. If the server is not running, only the
|
||||||
@@ -35,18 +54,6 @@ func (s *Server) Proc() *ResourceUsage {
|
|||||||
return &s.resources
|
return &s.resources
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) emitProcUsage() {
|
|
||||||
s.resources.mu.RLock()
|
|
||||||
defer s.resources.mu.RUnlock()
|
|
||||||
|
|
||||||
b, err := json.Marshal(s.resources)
|
|
||||||
if err == nil {
|
|
||||||
s.Events().Publish(StatsEvent, string(b))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: This might be a good place to add a debug log if stats are not sending.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the servers current state.
|
// Returns the servers current state.
|
||||||
func (ru *ResourceUsage) getInternalState() string {
|
func (ru *ResourceUsage) getInternalState() string {
|
||||||
ru.mu.RLock()
|
ru.mu.RLock()
|
||||||
@@ -62,8 +69,81 @@ func (ru *ResourceUsage) setInternalState(state string) {
|
|||||||
ru.mu.Unlock()
|
ru.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resets the usages values to zero, used when a server is stopped to ensure we don't hold
|
||||||
|
// onto any values incorrectly.
|
||||||
|
func (ru *ResourceUsage) Empty() {
|
||||||
|
ru.mu.Lock()
|
||||||
|
defer ru.mu.Unlock()
|
||||||
|
|
||||||
|
ru.Memory = 0
|
||||||
|
ru.CpuAbsolute = 0
|
||||||
|
ru.Network.TxBytes = 0
|
||||||
|
ru.Network.RxBytes = 0
|
||||||
|
}
|
||||||
|
|
||||||
func (ru *ResourceUsage) SetDisk(i int64) {
|
func (ru *ResourceUsage) SetDisk(i int64) {
|
||||||
ru.mu.Lock()
|
ru.mu.Lock()
|
||||||
|
defer ru.mu.Unlock()
|
||||||
|
|
||||||
ru.Disk = i
|
ru.Disk = i
|
||||||
ru.mu.Unlock()
|
}
|
||||||
|
|
||||||
|
func (ru *ResourceUsage) UpdateFromDocker(v *types.StatsJSON) {
|
||||||
|
ru.mu.Lock()
|
||||||
|
defer ru.mu.Unlock()
|
||||||
|
|
||||||
|
ru.CpuAbsolute = ru.calculateDockerAbsoluteCpu(&v.PreCPUStats, &v.CPUStats)
|
||||||
|
ru.Memory = ru.calculateDockerMemory(v.MemoryStats)
|
||||||
|
ru.MemoryLimit = v.MemoryStats.Limit
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ru *ResourceUsage) UpdateNetworkBytes(nw *types.NetworkStats) {
|
||||||
|
atomic.AddUint64(&ru.Network.RxBytes, nw.RxBytes)
|
||||||
|
atomic.AddUint64(&ru.Network.TxBytes, nw.TxBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The "docker stats" CLI call does not return the same value as the types.MemoryStats.Usage
|
||||||
|
// value which can be rather confusing to people trying to compare panel usage to
|
||||||
|
// their stats output.
|
||||||
|
//
|
||||||
|
// This math is straight up lifted from their CLI repository in order to show the same
|
||||||
|
// values to avoid people bothering me about it. It should also reflect a slightly more
|
||||||
|
// correct memory value anyways.
|
||||||
|
//
|
||||||
|
// @see https://github.com/docker/cli/blob/96e1d1d6/cli/command/container/stats_helpers.go#L227-L249
|
||||||
|
func (ru *ResourceUsage) calculateDockerMemory(stats types.MemoryStats) uint64 {
|
||||||
|
if v, ok := stats.Stats["total_inactive_file"]; ok && v < stats.Usage {
|
||||||
|
return stats.Usage - v
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := stats.Stats["inactive_file"]; v < stats.Usage {
|
||||||
|
return stats.Usage - v
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats.Usage
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculates the absolute CPU usage used by the server process on the system, not constrained
|
||||||
|
// by the defined CPU limits on the container.
|
||||||
|
//
|
||||||
|
// @see https://github.com/docker/cli/blob/aa097cf1aa19099da70930460250797c8920b709/cli/command/container/stats_helpers.go#L166
|
||||||
|
func (ru *ResourceUsage) calculateDockerAbsoluteCpu(pStats *types.CPUStats, stats *types.CPUStats) float64 {
|
||||||
|
// Calculate the change in CPU usage between the current and previous reading.
|
||||||
|
cpuDelta := float64(stats.CPUUsage.TotalUsage) - float64(pStats.CPUUsage.TotalUsage)
|
||||||
|
|
||||||
|
// Calculate the change for the entire system's CPU usage between current and previous reading.
|
||||||
|
systemDelta := float64(stats.SystemUsage) - float64(pStats.SystemUsage)
|
||||||
|
|
||||||
|
// Calculate the total number of CPU cores being used.
|
||||||
|
cpus := float64(stats.OnlineCPUs)
|
||||||
|
if cpus == 0.0 {
|
||||||
|
cpus = float64(len(stats.CPUUsage.PercpuUsage))
|
||||||
|
}
|
||||||
|
|
||||||
|
percent := 0.0
|
||||||
|
if systemDelta > 0.0 && cpuDelta > 0.0 {
|
||||||
|
percent = (cpuDelta / systemDelta) * cpus * 100.0
|
||||||
|
}
|
||||||
|
|
||||||
|
return math.Round(percent*1000) / 1000
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
|
"github.com/patrickmn/go-cache"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/pterodactyl/wings/api"
|
"github.com/pterodactyl/wings/api"
|
||||||
"github.com/pterodactyl/wings/environment"
|
|
||||||
"github.com/pterodactyl/wings/environment/docker"
|
|
||||||
"github.com/pterodactyl/wings/events"
|
|
||||||
"golang.org/x/sync/semaphore"
|
"golang.org/x/sync/semaphore"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -20,9 +19,6 @@ type Server struct {
|
|||||||
// Internal mutex used to block actions that need to occur sequentially, such as
|
// Internal mutex used to block actions that need to occur sequentially, such as
|
||||||
// writing the configuration to the disk.
|
// writing the configuration to the disk.
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
emitterLock sync.Mutex
|
|
||||||
powerLock *semaphore.Weighted
|
|
||||||
throttleLock sync.RWMutex
|
|
||||||
|
|
||||||
// Maintains the configuration for the server. This is the data that gets returned by the Panel
|
// Maintains the configuration for the server. This is the data that gets returned by the Panel
|
||||||
// such as build settings and container images.
|
// such as build settings and container images.
|
||||||
@@ -33,11 +29,15 @@ type Server struct {
|
|||||||
|
|
||||||
resources ResourceUsage
|
resources ResourceUsage
|
||||||
Archiver Archiver `json:"-"`
|
Archiver Archiver `json:"-"`
|
||||||
Environment environment.ProcessEnvironment `json:"-"`
|
Environment Environment `json:"-"`
|
||||||
Filesystem Filesystem `json:"-"`
|
Filesystem Filesystem `json:"-"`
|
||||||
|
|
||||||
|
// Server cache used to store frequently requested information in memory and make
|
||||||
|
// certain long operations return faster. For example, FS disk space usage.
|
||||||
|
cache *cache.Cache
|
||||||
|
|
||||||
// Events emitted by the server instance.
|
// Events emitted by the server instance.
|
||||||
emitter *events.EventBus
|
emitter *EventBus
|
||||||
|
|
||||||
// Defines the process configuration for the server instance. This is dynamically
|
// Defines the process configuration for the server instance. This is dynamically
|
||||||
// fetched from the Pterodactyl Server instance each time the server process is
|
// fetched from the Pterodactyl Server instance each time the server process is
|
||||||
@@ -49,9 +49,6 @@ type Server struct {
|
|||||||
// installation process, for example when a server is deleted from the panel while the
|
// installation process, for example when a server is deleted from the panel while the
|
||||||
// installer process is still running.
|
// installer process is still running.
|
||||||
installer InstallerDetails
|
installer InstallerDetails
|
||||||
|
|
||||||
// The console throttler instance used to control outputs.
|
|
||||||
throttler *ConsoleThrottler
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type InstallerDetails struct {
|
type InstallerDetails struct {
|
||||||
@@ -77,7 +74,7 @@ func (s *Server) GetEnvironmentVariables() []string {
|
|||||||
var out = []string{
|
var out = []string{
|
||||||
fmt.Sprintf("TZ=%s", zone),
|
fmt.Sprintf("TZ=%s", zone),
|
||||||
fmt.Sprintf("STARTUP=%s", s.Config().Invocation),
|
fmt.Sprintf("STARTUP=%s", s.Config().Invocation),
|
||||||
fmt.Sprintf("SERVER_MEMORY=%d", s.MemoryLimit()),
|
fmt.Sprintf("SERVER_MEMORY=%d", s.Build().MemoryLimit),
|
||||||
fmt.Sprintf("SERVER_IP=%s", s.Config().Allocations.DefaultMapping.Ip),
|
fmt.Sprintf("SERVER_IP=%s", s.Config().Allocations.DefaultMapping.Ip),
|
||||||
fmt.Sprintf("SERVER_PORT=%d", s.Config().Allocations.DefaultMapping.Port),
|
fmt.Sprintf("SERVER_PORT=%d", s.Config().Allocations.DefaultMapping.Port),
|
||||||
}
|
}
|
||||||
@@ -125,7 +122,7 @@ func (s *Server) Sync() error {
|
|||||||
|
|
||||||
func (s *Server) SyncWithConfiguration(cfg *api.ServerConfigurationResponse) error {
|
func (s *Server) SyncWithConfiguration(cfg *api.ServerConfigurationResponse) error {
|
||||||
// Update the data structure and persist it to the disk.
|
// Update the data structure and persist it to the disk.
|
||||||
if err := s.UpdateDataStructure(cfg.Settings); err != nil {
|
if err := s.UpdateDataStructure(cfg.Settings, false); err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,18 +130,11 @@ func (s *Server) SyncWithConfiguration(cfg *api.ServerConfigurationResponse) err
|
|||||||
s.procConfig = cfg.ProcessConfiguration
|
s.procConfig = cfg.ProcessConfiguration
|
||||||
s.Unlock()
|
s.Unlock()
|
||||||
|
|
||||||
// If this is a Docker environment we need to sync the stop configuration with it so that
|
|
||||||
// the process isn't just terminated when a user requests it be stopped.
|
|
||||||
if e, ok := s.Environment.(*docker.Environment); ok {
|
|
||||||
s.Log().Debug("syncing stop configuration with configured docker environment")
|
|
||||||
e.SetStopConfiguration(&cfg.ProcessConfiguration.Stop)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reads the log file for a server up to a specified number of bytes.
|
// Reads the log file for a server up to a specified number of bytes.
|
||||||
func (s *Server) ReadLogfile(len int) ([]string, error) {
|
func (s *Server) ReadLogfile(len int64) ([]string, error) {
|
||||||
return s.Environment.Readlog(len)
|
return s.Environment.Readlog(len)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,14 +146,9 @@ func (s *Server) IsBootable() bool {
|
|||||||
return exists
|
return exists
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initializes a server instance. This will run through and ensure that the environment
|
// Initalizes a server instance. This will run through and ensure that the environment
|
||||||
// for the server is setup, and that all of the necessary files are created.
|
// for the server is setup, and that all of the necessary files are created.
|
||||||
func (s *Server) CreateEnvironment() error {
|
func (s *Server) CreateEnvironment() error {
|
||||||
// Ensure the data directory exists before getting too far through this process.
|
|
||||||
if err := s.Filesystem.EnsureDataDirectory(); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.Environment.Create()
|
return s.Environment.Create()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,14 +157,24 @@ func (s *Server) GetProcessConfiguration() (*api.ServerConfigurationResponse, *a
|
|||||||
return api.NewRequester().GetServerConfiguration(s.Id())
|
return api.NewRequester().GetServerConfiguration(s.Id())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function that can receieve a power action and then process the
|
||||||
|
// actions that need to occur for it.
|
||||||
|
func (s *Server) HandlePowerAction(action PowerAction) error {
|
||||||
|
switch action.Action {
|
||||||
|
case "start":
|
||||||
|
return s.Environment.Start()
|
||||||
|
case "restart":
|
||||||
|
return s.Environment.Restart()
|
||||||
|
case "stop":
|
||||||
|
return s.Environment.Stop()
|
||||||
|
case "kill":
|
||||||
|
return s.Environment.Terminate(os.Kill)
|
||||||
|
default:
|
||||||
|
return errors.New("an invalid power action was provided")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Checks if the server is marked as being suspended or not on the system.
|
// Checks if the server is marked as being suspended or not on the system.
|
||||||
func (s *Server) IsSuspended() bool {
|
func (s *Server) IsSuspended() bool {
|
||||||
return s.Config().Suspended
|
return s.Config().Suspended
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) ProcessConfiguration() *api.ProcessConfiguration {
|
|
||||||
s.RLock()
|
|
||||||
defer s.RUnlock()
|
|
||||||
|
|
||||||
return s.procConfig
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/pterodactyl/wings/config"
|
"github.com/pterodactyl/wings/config"
|
||||||
"github.com/pterodactyl/wings/environment"
|
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
@@ -14,6 +13,13 @@ import (
|
|||||||
|
|
||||||
var stateMutex sync.Mutex
|
var stateMutex sync.Mutex
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProcessOfflineState = "offline"
|
||||||
|
ProcessStartingState = "starting"
|
||||||
|
ProcessRunningState = "running"
|
||||||
|
ProcessStoppingState = "stopping"
|
||||||
|
)
|
||||||
|
|
||||||
// Returns the state of the servers.
|
// Returns the state of the servers.
|
||||||
func getServerStates() (map[string]string, error) {
|
func getServerStates() (map[string]string, error) {
|
||||||
// Request a lock after we check if the file exists.
|
// Request a lock after we check if the file exists.
|
||||||
@@ -64,10 +70,7 @@ func saveServerStates() error {
|
|||||||
// Sets the state of the server internally. This function handles crash detection as
|
// Sets the state of the server internally. This function handles crash detection as
|
||||||
// well as reporting to event listeners for the server.
|
// well as reporting to event listeners for the server.
|
||||||
func (s *Server) SetState(state string) error {
|
func (s *Server) SetState(state string) error {
|
||||||
if state != environment.ProcessOfflineState &&
|
if state != ProcessOfflineState && state != ProcessStartingState && state != ProcessRunningState && state != ProcessStoppingState {
|
||||||
state != environment.ProcessStartingState &&
|
|
||||||
state != environment.ProcessRunningState &&
|
|
||||||
state != environment.ProcessStoppingState {
|
|
||||||
return errors.New(fmt.Sprintf("invalid server state received: %s", state))
|
return errors.New(fmt.Sprintf("invalid server state received: %s", state))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,16 +99,6 @@ func (s *Server) SetState(state string) error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Reset the resource usage to 0 when the process fully stops so that all of the UI
|
|
||||||
// views in the Panel correctly display 0.
|
|
||||||
if state == environment.ProcessOfflineState {
|
|
||||||
s.resources.mu.Lock()
|
|
||||||
s.resources.Empty()
|
|
||||||
s.resources.mu.Unlock()
|
|
||||||
|
|
||||||
s.emitProcUsage()
|
|
||||||
}
|
|
||||||
|
|
||||||
// If server was in an online state, and is now in an offline state we should handle
|
// If server was in an online state, and is now in an offline state we should handle
|
||||||
// that as a crash event. In that scenario, check the last crash time, and the crash
|
// that as a crash event. In that scenario, check the last crash time, and the crash
|
||||||
// counter.
|
// counter.
|
||||||
@@ -114,7 +107,7 @@ func (s *Server) SetState(state string) error {
|
|||||||
// automatically attempt to start the process back up for the user. This is done in a
|
// automatically attempt to start the process back up for the user. This is done in a
|
||||||
// separate thread as to not block any actions currently taking place in the flow
|
// separate thread as to not block any actions currently taking place in the flow
|
||||||
// that called this function.
|
// that called this function.
|
||||||
if (prevState == environment.ProcessStartingState || prevState == environment.ProcessRunningState) && s.GetState() == environment.ProcessOfflineState {
|
if (prevState == ProcessStartingState || prevState == ProcessRunningState) && s.GetState() == ProcessOfflineState {
|
||||||
s.Log().Info("detected server as entering a crashed state; running crash handler")
|
s.Log().Info("detected server as entering a crashed state; running crash handler")
|
||||||
|
|
||||||
go func(server *Server) {
|
go func(server *Server) {
|
||||||
@@ -142,5 +135,5 @@ func (s *Server) GetState() string {
|
|||||||
func (s *Server) IsRunning() bool {
|
func (s *Server) IsRunning() bool {
|
||||||
st := s.GetState()
|
st := s.GetState()
|
||||||
|
|
||||||
return st == environment.ProcessRunningState || st == environment.ProcessStartingState
|
return st == ProcessRunningState || st == ProcessStartingState
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"github.com/buger/jsonparser"
|
"github.com/buger/jsonparser"
|
||||||
"github.com/imdario/mergo"
|
"github.com/imdario/mergo"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/pterodactyl/wings/environment"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Merges data passed through in JSON form into the existing server object.
|
// Merges data passed through in JSON form into the existing server object.
|
||||||
@@ -15,7 +14,7 @@ import (
|
|||||||
// The server will be marked as requiring a rebuild on the next boot sequence,
|
// The server will be marked as requiring a rebuild on the next boot sequence,
|
||||||
// it is up to the specific environment to determine what needs to happen when
|
// it is up to the specific environment to determine what needs to happen when
|
||||||
// that is the case.
|
// that is the case.
|
||||||
func (s *Server) UpdateDataStructure(data []byte) error {
|
func (s *Server) UpdateDataStructure(data []byte, background bool) error {
|
||||||
src := new(Configuration)
|
src := new(Configuration)
|
||||||
if err := json.Unmarshal(data, src); err != nil {
|
if err := json.Unmarshal(data, src); err != nil {
|
||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
@@ -31,8 +30,8 @@ func (s *Server) UpdateDataStructure(data []byte) error {
|
|||||||
// Grab a copy of the configuration to work on.
|
// Grab a copy of the configuration to work on.
|
||||||
c := *s.Config()
|
c := *s.Config()
|
||||||
|
|
||||||
// Lock our copy of the configuration since the deferred unlock will end up acting upon this
|
// Lock our copy of the configuration since the defered unlock will end up acting upon this
|
||||||
// new memory address rather than the old one. If we don't lock this, the deferred unlock will
|
// new memory address rather than the old one. If we don't lock this, the defered unlock will
|
||||||
// cause a panic when it goes to run. However, since we only update s.cfg at the end, if there
|
// cause a panic when it goes to run. However, since we only update s.cfg at the end, if there
|
||||||
// is an error before that point we'll still properly unlock the original configuration for the
|
// is an error before that point we'll still properly unlock the original configuration for the
|
||||||
// server.
|
// server.
|
||||||
@@ -68,7 +67,7 @@ func (s *Server) UpdateDataStructure(data []byte) error {
|
|||||||
return errors.WithStack(err)
|
return errors.WithStack(err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
c.Build.OOMDisabled = v
|
c.Container.OomDisabled = v
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mergo also cannot handle this boolean value.
|
// Mergo also cannot handle this boolean value.
|
||||||
@@ -80,14 +79,6 @@ func (s *Server) UpdateDataStructure(data []byte) error {
|
|||||||
c.Suspended = v
|
c.Suspended = v
|
||||||
}
|
}
|
||||||
|
|
||||||
if v, err := jsonparser.GetBoolean(data, "skip_egg_scripts"); err != nil {
|
|
||||||
if err != jsonparser.KeyPathNotFoundError {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
c.SkipEggScripts = v
|
|
||||||
}
|
|
||||||
|
|
||||||
// Environment and Mappings should be treated as a full update at all times, never a
|
// Environment and Mappings should be treated as a full update at all times, never a
|
||||||
// true patch, otherwise we can't know what we're passing along.
|
// true patch, otherwise we can't know what we're passing along.
|
||||||
if src.EnvVars != nil && len(src.EnvVars) > 0 {
|
if src.EnvVars != nil && len(src.EnvVars) > 0 {
|
||||||
@@ -105,52 +96,36 @@ func (s *Server) UpdateDataStructure(data []byte) error {
|
|||||||
// Update the configuration once we have a lock on the configuration object.
|
// Update the configuration once we have a lock on the configuration object.
|
||||||
s.cfg = c
|
s.cfg = c
|
||||||
|
|
||||||
|
if background {
|
||||||
|
go s.runBackgroundActions()
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updates the environment for the server to match any of the changed data. This pushes new settings and
|
// Runs through different actions once a server's configuration has been persisted
|
||||||
// environment variables to the environment. In addition, the in-situ update method is called on the
|
// to the disk. This function does not return anything as any failures should be logged
|
||||||
// environment which will allow environments that make use of it (such as Docker) to immediately apply
|
// but have no effect on actually updating the server itself.
|
||||||
// some settings without having to wait on a server to restart.
|
|
||||||
//
|
//
|
||||||
// This functionality allows a server's resources limits to be modified on the fly and have them apply
|
// These tasks run in independent threads where relevant to speed up any updates
|
||||||
// right away allowing for dynamic resource allocation and responses to abusive server processes.
|
// that need to happen.
|
||||||
func (s *Server) SyncWithEnvironment() {
|
func (s *Server) runBackgroundActions() {
|
||||||
s.Log().Debug("syncing server settings with environment")
|
// Check if the s is now suspended, and if so and the process is not terminated
|
||||||
|
// yet, do it immediately.
|
||||||
|
if s.IsSuspended() && s.GetState() != ProcessOfflineState {
|
||||||
|
s.Log().Info("server suspended with running process state, terminating now")
|
||||||
|
|
||||||
// Update the environment settings using the new information from this server.
|
if err := s.Environment.WaitForStop(10, true); err != nil {
|
||||||
s.Environment.Config().SetSettings(environment.Settings{
|
s.Log().WithField("error", err).Warn("failed to terminate server environment after suspension")
|
||||||
Mounts: s.Mounts(),
|
}
|
||||||
Allocations: s.Config().Allocations,
|
}
|
||||||
Limits: s.Config().Build,
|
|
||||||
})
|
|
||||||
|
|
||||||
// If build limits are changed, environment variables also change. Plus, any modifications to
|
|
||||||
// the startup command also need to be properly propagated to this environment.
|
|
||||||
//
|
|
||||||
// @see https://github.com/pterodactyl/panel/issues/2255
|
|
||||||
s.Environment.Config().SetEnvironmentVariables(s.GetEnvironmentVariables())
|
|
||||||
|
|
||||||
if !s.IsSuspended() {
|
if !s.IsSuspended() {
|
||||||
// Update the environment in place, allowing memory and CPU usage to be adjusted
|
// Update the environment in place, allowing memory and CPU usage to be adjusted
|
||||||
// on the fly without the user needing to reboot (theoretically).
|
// on the fly without the user needing to reboot (theoretically).
|
||||||
s.Log().Info("performing server limit modification on-the-fly")
|
s.Log().Info("performing server limit modification on-the-fly")
|
||||||
if err := s.Environment.InSituUpdate(); err != nil {
|
if err := s.Environment.InSituUpdate(); err != nil {
|
||||||
// This is not a failure, the process is still running fine and will fix itself on the
|
|
||||||
// next boot, or fail out entirely in a more logical position.
|
|
||||||
s.Log().WithField("error", err).Warn("failed to perform on-the-fly update of the server environment")
|
s.Log().WithField("error", err).Warn("failed to perform on-the-fly update of the server environment")
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Checks if the server is now in a suspended state. If so and a server process is currently running it
|
|
||||||
// will be gracefully stopped (and terminated if it refuses to stop).
|
|
||||||
if s.GetState() != environment.ProcessOfflineState {
|
|
||||||
s.Log().Info("server suspended with running process state, terminating now")
|
|
||||||
|
|
||||||
go func(s *Server) {
|
|
||||||
if err := s.Environment.WaitForStop(60, true); err != nil {
|
|
||||||
s.Log().WithField("error", err).Warn("failed to terminate server environment after suspension")
|
|
||||||
}
|
|
||||||
}(s)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
package sftp
|
|
||||||
|
|
||||||
type fxerr uint32
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Extends the default SFTP server to return a quota exceeded error to the client.
|
|
||||||
//
|
|
||||||
// @see https://tools.ietf.org/id/draft-ietf-secsh-filexfer-13.txt
|
|
||||||
ErrSshQuotaExceeded = fxerr(15)
|
|
||||||
)
|
|
||||||
|
|
||||||
func (e fxerr) Error() string {
|
|
||||||
switch e {
|
|
||||||
case ErrSshQuotaExceeded:
|
|
||||||
return "Quota Exceeded"
|
|
||||||
default:
|
|
||||||
return "Failure"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
380
sftp/handler.go
380
sftp/handler.go
@@ -1,380 +0,0 @@
|
|||||||
package sftp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/apex/log"
|
|
||||||
"github.com/patrickmn/go-cache"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/pkg/sftp"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
type FileSystem struct {
|
|
||||||
UUID string
|
|
||||||
Permissions []string
|
|
||||||
ReadOnly bool
|
|
||||||
User User
|
|
||||||
Cache *cache.Cache
|
|
||||||
|
|
||||||
PathValidator func(fs FileSystem, p string) (string, error)
|
|
||||||
HasDiskSpace func(fs FileSystem) bool
|
|
||||||
|
|
||||||
logger *log.Entry
|
|
||||||
lock sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs FileSystem) buildPath(p string) (string, error) {
|
|
||||||
return fs.PathValidator(fs, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
PermissionFileRead = "file.read"
|
|
||||||
PermissionFileReadContent = "file.read-content"
|
|
||||||
PermissionFileCreate = "file.create"
|
|
||||||
PermissionFileUpdate = "file.update"
|
|
||||||
PermissionFileDelete = "file.delete"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Fileread creates a reader for a file on the system and returns the reader back.
|
|
||||||
func (fs FileSystem) Fileread(request *sftp.Request) (io.ReaderAt, error) {
|
|
||||||
// Check first if the user can actually open and view a file. This permission is named
|
|
||||||
// really poorly, but it is checking if they can read. There is an addition permission,
|
|
||||||
// "save-files" which determines if they can write that file.
|
|
||||||
if !fs.can(PermissionFileReadContent) {
|
|
||||||
return nil, sftp.ErrSshFxPermissionDenied
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := fs.buildPath(request.Filepath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, sftp.ErrSshFxNoSuchFile
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.lock.Lock()
|
|
||||||
defer fs.lock.Unlock()
|
|
||||||
|
|
||||||
if _, err := os.Stat(p); os.IsNotExist(err) {
|
|
||||||
return nil, sftp.ErrSshFxNoSuchFile
|
|
||||||
} else if err != nil {
|
|
||||||
fs.logger.WithField("error", errors.WithStack(err)).Error("error while processing file stat")
|
|
||||||
|
|
||||||
return nil, sftp.ErrSshFxFailure
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := os.Open(p)
|
|
||||||
if err != nil {
|
|
||||||
fs.logger.WithField("source", p).WithField("error", errors.WithStack(err)).Error("could not open file for reading")
|
|
||||||
return nil, sftp.ErrSshFxFailure
|
|
||||||
}
|
|
||||||
|
|
||||||
return file, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filewrite handles the write actions for a file on the system.
|
|
||||||
func (fs FileSystem) Filewrite(request *sftp.Request) (io.WriterAt, error) {
|
|
||||||
if fs.ReadOnly {
|
|
||||||
return nil, sftp.ErrSshFxOpUnsupported
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := fs.buildPath(request.Filepath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, sftp.ErrSshFxNoSuchFile
|
|
||||||
}
|
|
||||||
|
|
||||||
var l = fs.logger.WithField("source", p)
|
|
||||||
|
|
||||||
// If the user doesn't have enough space left on the server it should respond with an
|
|
||||||
// error since we won't be letting them write this file to the disk.
|
|
||||||
if !fs.HasDiskSpace(fs) {
|
|
||||||
return nil, ErrSshQuotaExceeded
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.lock.Lock()
|
|
||||||
defer fs.lock.Unlock()
|
|
||||||
|
|
||||||
stat, statErr := os.Stat(p)
|
|
||||||
// If the file doesn't exist we need to create it, as well as the directory pathway
|
|
||||||
// leading up to where that file will be created.
|
|
||||||
if os.IsNotExist(statErr) {
|
|
||||||
// This is a different pathway than just editing an existing file. If it doesn't exist already
|
|
||||||
// we need to determine if this user has permission to create files.
|
|
||||||
if !fs.can(PermissionFileCreate) {
|
|
||||||
return nil, sftp.ErrSshFxPermissionDenied
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create all of the directories leading up to the location where this file is being created.
|
|
||||||
if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil {
|
|
||||||
l.WithFields(log.Fields{
|
|
||||||
"path": filepath.Dir(p),
|
|
||||||
"error": errors.WithStack(err),
|
|
||||||
}).Error("error making path for file")
|
|
||||||
|
|
||||||
return nil, sftp.ErrSshFxFailure
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := os.Create(p)
|
|
||||||
if err != nil {
|
|
||||||
l.WithField("error", errors.WithStack(err)).Error("failed to create file")
|
|
||||||
|
|
||||||
return nil, sftp.ErrSshFxFailure
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not failing here is intentional. We still made the file, it is just owned incorrectly
|
|
||||||
// and will likely cause some issues.
|
|
||||||
if err := os.Chown(p, fs.User.Uid, fs.User.Gid); err != nil {
|
|
||||||
l.WithField("error", errors.WithStack(err)).Warn("failed to set permissions on file")
|
|
||||||
}
|
|
||||||
|
|
||||||
return file, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the stat error isn't about the file not existing, there is some other issue
|
|
||||||
// at play and we need to go ahead and bail out of the process.
|
|
||||||
if statErr != nil {
|
|
||||||
l.WithField("error", errors.WithStack(statErr)).Error("encountered error performing file stat")
|
|
||||||
|
|
||||||
return nil, sftp.ErrSshFxFailure
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we've made it here it means the file already exists and we don't need to do anything
|
|
||||||
// fancy to handle it. Just pass over the request flags so the system knows what the end
|
|
||||||
// goal with the file is going to be.
|
|
||||||
//
|
|
||||||
// But first, check that the user has permission to save modified files.
|
|
||||||
if !fs.can(PermissionFileUpdate) {
|
|
||||||
return nil, sftp.ErrSshFxPermissionDenied
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not sure this would ever happen, but lets not find out.
|
|
||||||
if stat.IsDir() {
|
|
||||||
return nil, sftp.ErrSshFxOpUnsupported
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := os.Create(p)
|
|
||||||
if err != nil {
|
|
||||||
// Prevent errors if the file is deleted between the stat and this call.
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil, sftp.ErrSSHFxNoSuchFile
|
|
||||||
}
|
|
||||||
|
|
||||||
l.WithField("flags", request.Flags).WithField("error", errors.WithStack(err)).Error("failed to open existing file on system")
|
|
||||||
return nil, sftp.ErrSshFxFailure
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not failing here is intentional. We still made the file, it is just owned incorrectly
|
|
||||||
// and will likely cause some issues.
|
|
||||||
if err := os.Chown(p, fs.User.Uid, fs.User.Gid); err != nil {
|
|
||||||
l.WithField("error", errors.WithStack(err)).Warn("error chowning file")
|
|
||||||
}
|
|
||||||
|
|
||||||
return file, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filecmd hander for basic SFTP system calls related to files, but not anything to do with reading
|
|
||||||
// or writing to those files.
|
|
||||||
func (fs FileSystem) Filecmd(request *sftp.Request) error {
|
|
||||||
if fs.ReadOnly {
|
|
||||||
return sftp.ErrSshFxOpUnsupported
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := fs.buildPath(request.Filepath)
|
|
||||||
if err != nil {
|
|
||||||
return sftp.ErrSshFxNoSuchFile
|
|
||||||
}
|
|
||||||
|
|
||||||
var l = fs.logger.WithField("source", p)
|
|
||||||
|
|
||||||
var target string
|
|
||||||
// If a target is provided in this request validate that it is going to the correct
|
|
||||||
// location for the server. If it is not, return an operation unsupported error. This
|
|
||||||
// is maybe not the best error response, but its not wrong either.
|
|
||||||
if request.Target != "" {
|
|
||||||
target, err = fs.buildPath(request.Target)
|
|
||||||
if err != nil {
|
|
||||||
return sftp.ErrSshFxOpUnsupported
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch request.Method {
|
|
||||||
case "Setstat":
|
|
||||||
if !fs.can(PermissionFileUpdate) {
|
|
||||||
return sftp.ErrSshFxPermissionDenied
|
|
||||||
}
|
|
||||||
|
|
||||||
var mode os.FileMode = 0644
|
|
||||||
// If the client passed a valid file permission use that, otherwise use the
|
|
||||||
// default of 0644 set above.
|
|
||||||
if request.Attributes().FileMode().Perm() != 0000 {
|
|
||||||
mode = request.Attributes().FileMode().Perm()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force directories to be 0755
|
|
||||||
if request.Attributes().FileMode().IsDir() {
|
|
||||||
mode = 0755
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.Chmod(p, mode); err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return sftp.ErrSSHFxNoSuchFile
|
|
||||||
}
|
|
||||||
|
|
||||||
l.WithField("error", errors.WithStack(err)).Error("failed to perform setstat on item")
|
|
||||||
return sftp.ErrSSHFxFailure
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case "Rename":
|
|
||||||
if !fs.can(PermissionFileUpdate) {
|
|
||||||
return sftp.ErrSSHFxPermissionDenied
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.Rename(p, target); err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return sftp.ErrSSHFxNoSuchFile
|
|
||||||
}
|
|
||||||
|
|
||||||
l.WithField("target", target).WithField("error", errors.WithStack(err)).Error("failed to rename file")
|
|
||||||
|
|
||||||
return sftp.ErrSshFxFailure
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
case "Rmdir":
|
|
||||||
if !fs.can(PermissionFileDelete) {
|
|
||||||
return sftp.ErrSshFxPermissionDenied
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.RemoveAll(p); err != nil {
|
|
||||||
l.WithField("error", errors.WithStack(err)).Error("failed to remove directory")
|
|
||||||
|
|
||||||
return sftp.ErrSshFxFailure
|
|
||||||
}
|
|
||||||
|
|
||||||
return sftp.ErrSshFxOk
|
|
||||||
case "Mkdir":
|
|
||||||
if !fs.can(PermissionFileCreate) {
|
|
||||||
return sftp.ErrSshFxPermissionDenied
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.MkdirAll(p, 0755); err != nil {
|
|
||||||
l.WithField("error", errors.WithStack(err)).Error("failed to create directory")
|
|
||||||
|
|
||||||
return sftp.ErrSshFxFailure
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
case "Symlink":
|
|
||||||
if !fs.can(PermissionFileCreate) {
|
|
||||||
return sftp.ErrSshFxPermissionDenied
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.Symlink(p, target); err != nil {
|
|
||||||
l.WithField("target", target).WithField("error", errors.WithStack(err)).Error("failed to create symlink")
|
|
||||||
|
|
||||||
return sftp.ErrSshFxFailure
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
case "Remove":
|
|
||||||
if !fs.can(PermissionFileDelete) {
|
|
||||||
return sftp.ErrSshFxPermissionDenied
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.Remove(p); err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return sftp.ErrSSHFxNoSuchFile
|
|
||||||
}
|
|
||||||
|
|
||||||
l.WithField("error", errors.WithStack(err)).Error("failed to remove a file")
|
|
||||||
|
|
||||||
return sftp.ErrSshFxFailure
|
|
||||||
}
|
|
||||||
|
|
||||||
return sftp.ErrSshFxOk
|
|
||||||
default:
|
|
||||||
return sftp.ErrSshFxOpUnsupported
|
|
||||||
}
|
|
||||||
|
|
||||||
var fileLocation = p
|
|
||||||
if target != "" {
|
|
||||||
fileLocation = target
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not failing here is intentional. We still made the file, it is just owned incorrectly
|
|
||||||
// and will likely cause some issues. There is no logical check for if the file was removed
|
|
||||||
// because both of those cases (Rmdir, Remove) have an explicit return rather than break.
|
|
||||||
if err := os.Chown(fileLocation, fs.User.Uid, fs.User.Gid); err != nil {
|
|
||||||
l.WithField("error", errors.WithStack(err)).Warn("error chowning file")
|
|
||||||
}
|
|
||||||
|
|
||||||
return sftp.ErrSshFxOk
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filelist is the handler for SFTP filesystem list calls. This will handle calls to list the contents of
|
|
||||||
// a directory as well as perform file/folder stat calls.
|
|
||||||
func (fs FileSystem) Filelist(request *sftp.Request) (sftp.ListerAt, error) {
|
|
||||||
p, err := fs.buildPath(request.Filepath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, sftp.ErrSshFxNoSuchFile
|
|
||||||
}
|
|
||||||
|
|
||||||
switch request.Method {
|
|
||||||
case "List":
|
|
||||||
if !fs.can(PermissionFileRead) {
|
|
||||||
return nil, sftp.ErrSshFxPermissionDenied
|
|
||||||
}
|
|
||||||
|
|
||||||
files, err := ioutil.ReadDir(p)
|
|
||||||
if err != nil {
|
|
||||||
fs.logger.WithField("error", errors.WithStack(err)).Error("error while listing directory")
|
|
||||||
|
|
||||||
return nil, sftp.ErrSshFxFailure
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListerAt(files), nil
|
|
||||||
case "Stat":
|
|
||||||
if !fs.can(PermissionFileRead) {
|
|
||||||
return nil, sftp.ErrSshFxPermissionDenied
|
|
||||||
}
|
|
||||||
|
|
||||||
s, err := os.Stat(p)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil, sftp.ErrSshFxNoSuchFile
|
|
||||||
} else if err != nil {
|
|
||||||
fs.logger.WithField("source", p).WithField("error", errors.WithStack(err)).Error("error performing stat on file")
|
|
||||||
|
|
||||||
return nil, sftp.ErrSshFxFailure
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListerAt([]os.FileInfo{s}), nil
|
|
||||||
default:
|
|
||||||
// Before adding readlink support we need to evaluate any potential security risks
|
|
||||||
// as a result of navigating around to a location that is outside the home directory
|
|
||||||
// for the logged in user. I don't foresee it being much of a problem, but I do want to
|
|
||||||
// check it out before slapping some code here. Until then, we'll just return an
|
|
||||||
// unsupported response code.
|
|
||||||
return nil, sftp.ErrSshFxOpUnsupported
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determines if a user has permission to perform a specific action on the SFTP server. These
|
|
||||||
// permissions are defined and returned by the Panel API.
|
|
||||||
func (fs FileSystem) can(permission string) bool {
|
|
||||||
// Server owners and super admins have their permissions returned as '[*]' via the Panel
|
|
||||||
// API, so for the sake of speed do an initial check for that before iterating over the
|
|
||||||
// entire array of permissions.
|
|
||||||
if len(fs.Permissions) == 1 && fs.Permissions[0] == "*" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not the owner or an admin, loop over the permissions that were returned to determine
|
|
||||||
// if they have the passed permission.
|
|
||||||
for _, p := range fs.Permissions {
|
|
||||||
if p == permission {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package sftp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ListerAt []os.FileInfo
|
|
||||||
|
|
||||||
// Returns the number of entries copied and an io.EOF error if we made it to the end of the file list.
|
|
||||||
// Take a look at the pkg/sftp godoc for more information about how this function should work.
|
|
||||||
func (l ListerAt) ListAt(f []os.FileInfo, offset int64) (int, error) {
|
|
||||||
if offset >= int64(len(l)) {
|
|
||||||
return 0, io.EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
if n := copy(f, l[offset:]); n < len(f) {
|
|
||||||
return n, io.EOF
|
|
||||||
} else {
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
326
sftp/server.go
326
sftp/server.go
@@ -1,238 +1,118 @@
|
|||||||
package sftp
|
package sftp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/pem"
|
|
||||||
"fmt"
|
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/patrickmn/go-cache"
|
"github.com/pkg/errors"
|
||||||
"github.com/pkg/sftp"
|
"github.com/pterodactyl/sftp-server"
|
||||||
"github.com/pterodactyl/wings/api"
|
"github.com/pterodactyl/wings/api"
|
||||||
"golang.org/x/crypto/ssh"
|
"github.com/pterodactyl/wings/config"
|
||||||
"io"
|
"github.com/pterodactyl/wings/server"
|
||||||
"io/ioutil"
|
"go.uber.org/zap"
|
||||||
"net"
|
"regexp"
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Settings struct {
|
func Initialize(config *config.Configuration) error {
|
||||||
BasePath string
|
c := &sftp_server.Server{
|
||||||
ReadOnly bool
|
User: sftp_server.SftpUser{
|
||||||
BindPort int
|
Uid: config.System.User.Uid,
|
||||||
BindAddress string
|
Gid: config.System.User.Gid,
|
||||||
}
|
},
|
||||||
|
Settings: sftp_server.Settings{
|
||||||
|
BasePath: config.System.Data,
|
||||||
|
ReadOnly: config.System.Sftp.ReadOnly,
|
||||||
|
BindAddress: config.System.Sftp.Address,
|
||||||
|
BindPort: config.System.Sftp.Port,
|
||||||
|
},
|
||||||
|
CredentialValidator: validateCredentials,
|
||||||
|
PathValidator: validatePath,
|
||||||
|
DiskSpaceValidator: validateDiskSpace,
|
||||||
|
}
|
||||||
|
|
||||||
type User struct {
|
if err := sftp_server.New(c); err != nil {
|
||||||
Uid int
|
return err
|
||||||
Gid int
|
}
|
||||||
}
|
|
||||||
|
|
||||||
type Server struct {
|
c.ConfigureLogger(func() *zap.SugaredLogger {
|
||||||
cache *cache.Cache
|
return zap.S().Named("sftp")
|
||||||
|
|
||||||
Settings Settings
|
|
||||||
User User
|
|
||||||
|
|
||||||
PathValidator func(fs FileSystem, p string) (string, error)
|
|
||||||
DiskSpaceValidator func(fs FileSystem) bool
|
|
||||||
|
|
||||||
// Validator function that is called when a user connects to the server. This should
|
|
||||||
// check against whatever system is desired to confirm if the given username and password
|
|
||||||
// combination is valid. If so, should return an authentication response.
|
|
||||||
CredentialValidator func(r api.SftpAuthRequest) (*api.SftpAuthResponse, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new server configuration instance.
|
|
||||||
func New(c *Server) error {
|
|
||||||
c.cache = cache.New(5*time.Minute, 10*time.Minute)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the SFTP server and add a persistent listener to handle inbound SFTP connections.
|
|
||||||
func (c *Server) Initialize() error {
|
|
||||||
serverConfig := &ssh.ServerConfig{
|
|
||||||
NoClientAuth: false,
|
|
||||||
MaxAuthTries: 6,
|
|
||||||
PasswordCallback: func(conn ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
|
|
||||||
resp, err := c.CredentialValidator(api.SftpAuthRequest{
|
|
||||||
User: conn.User(),
|
|
||||||
Pass: string(pass),
|
|
||||||
IP: conn.RemoteAddr().String(),
|
|
||||||
SessionID: conn.SessionID(),
|
|
||||||
ClientVersion: conn.ClientVersion(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
// Initialize the SFTP server in a background thread since this is
|
||||||
return nil, err
|
// a long running operation.
|
||||||
}
|
go func(instance *sftp_server.Server) {
|
||||||
|
if err := c.Initalize(); err != nil {
|
||||||
sshPerm := &ssh.Permissions{
|
log.WithField("subsystem", "sftp").WithField("error", errors.WithStack(err)).Error("failed to initialize SFTP subsystem")
|
||||||
Extensions: map[string]string{
|
|
||||||
"uuid": resp.Server,
|
|
||||||
"user": conn.User(),
|
|
||||||
"permissions": strings.Join(resp.Permissions, ","),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return sshPerm, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := os.Stat(path.Join(c.Settings.BasePath, ".sftp/id_rsa")); os.IsNotExist(err) {
|
|
||||||
if err := c.generatePrivateKey(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
privateBytes, err := ioutil.ReadFile(path.Join(c.Settings.BasePath, ".sftp/id_rsa"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
private, err := ssh.ParsePrivateKey(privateBytes)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add our private key to the server configuration.
|
|
||||||
serverConfig.AddHostKey(private)
|
|
||||||
|
|
||||||
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", c.Settings.BindAddress, c.Settings.BindPort))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.WithField("host", c.Settings.BindAddress).WithField("port", c.Settings.BindPort).Info("sftp subsystem listening for connections")
|
|
||||||
|
|
||||||
for {
|
|
||||||
conn, _ := listener.Accept()
|
|
||||||
if conn != nil {
|
|
||||||
go c.AcceptInboundConnection(conn, serverConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handles an inbound connection to the instance and determines if we should serve the request
|
|
||||||
// or not.
|
|
||||||
func (c Server) AcceptInboundConnection(conn net.Conn, config *ssh.ServerConfig) {
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
// Before beginning a handshake must be performed on the incoming net.Conn
|
|
||||||
sconn, chans, reqs, err := ssh.NewServerConn(conn, config)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer sconn.Close()
|
|
||||||
|
|
||||||
go ssh.DiscardRequests(reqs)
|
|
||||||
|
|
||||||
for newChannel := range chans {
|
|
||||||
// If its not a session channel we just move on because its not something we
|
|
||||||
// know how to handle at this point.
|
|
||||||
if newChannel.ChannelType() != "session" {
|
|
||||||
newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
channel, requests, err := newChannel.Accept()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Channels have a type that is dependent on the protocol. For SFTP this is "subsystem"
|
|
||||||
// with a payload that (should) be "sftp". Discard anything else we receive ("pty", "shell", etc)
|
|
||||||
go func(in <-chan *ssh.Request) {
|
|
||||||
for req := range in {
|
|
||||||
ok := false
|
|
||||||
|
|
||||||
switch req.Type {
|
|
||||||
case "subsystem":
|
|
||||||
if string(req.Payload[4:]) == "sftp" {
|
|
||||||
ok = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Reply(ok, nil)
|
|
||||||
}
|
|
||||||
}(requests)
|
|
||||||
|
|
||||||
// Configure the user's home folder for the rest of the request cycle.
|
|
||||||
if sconn.Permissions.Extensions["uuid"] == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new handler for the currently logged in user's server.
|
|
||||||
fs := c.createHandler(sconn)
|
|
||||||
|
|
||||||
// Create the server instance for the channel using the filesystem we created above.
|
|
||||||
server := sftp.NewRequestServer(channel, fs)
|
|
||||||
|
|
||||||
if err := server.Serve(); err == io.EOF {
|
|
||||||
server.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a new SFTP handler for a given server. The directory argument should
|
|
||||||
// be the base directory for a server. All actions done on the server will be
|
|
||||||
// relative to that directory, and the user will not be able to escape out of it.
|
|
||||||
func (c Server) createHandler(sc *ssh.ServerConn) sftp.Handlers {
|
|
||||||
p := FileSystem{
|
|
||||||
UUID: sc.Permissions.Extensions["uuid"],
|
|
||||||
Permissions: strings.Split(sc.Permissions.Extensions["permissions"], ","),
|
|
||||||
ReadOnly: c.Settings.ReadOnly,
|
|
||||||
Cache: c.cache,
|
|
||||||
User: c.User,
|
|
||||||
HasDiskSpace: c.DiskSpaceValidator,
|
|
||||||
PathValidator: c.PathValidator,
|
|
||||||
logger: log.WithFields(log.Fields{
|
|
||||||
"subsystem": "sftp",
|
|
||||||
"username": sc.User(),
|
|
||||||
"ip": sc.RemoteAddr(),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
return sftp.Handlers{
|
|
||||||
FileGet: p,
|
|
||||||
FilePut: p,
|
|
||||||
FileCmd: p,
|
|
||||||
FileList: p,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generates a private key that will be used by the SFTP server.
|
|
||||||
func (c Server) generatePrivateKey() error {
|
|
||||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.MkdirAll(path.Join(c.Settings.BasePath, ".sftp"), 0755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
o, err := os.OpenFile(path.Join(c.Settings.BasePath, ".sftp/id_rsa"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer o.Close()
|
|
||||||
|
|
||||||
pkey := &pem.Block{
|
|
||||||
Type: "RSA PRIVATE KEY",
|
|
||||||
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := pem.Encode(o, pkey); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
}(c)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validatePath(fs sftp_server.FileSystem, p string) (string, error) {
|
||||||
|
s := server.GetServers().Find(func(server *server.Server) bool {
|
||||||
|
return server.Id() == fs.UUID
|
||||||
|
})
|
||||||
|
|
||||||
|
if s == nil {
|
||||||
|
return "", errors.New("no server found with that UUID")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Filesystem.SafePath(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateDiskSpace(fs sftp_server.FileSystem) bool {
|
||||||
|
s := server.GetServers().Find(func(server *server.Server) bool {
|
||||||
|
return server.Id() == fs.UUID
|
||||||
|
})
|
||||||
|
|
||||||
|
if s == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Filesystem.HasSpaceAvailable()
|
||||||
|
}
|
||||||
|
|
||||||
|
var validUsernameRegexp = regexp.MustCompile(`^(?i)(.+)\.([a-z0-9]{8})$`)
|
||||||
|
|
||||||
|
// Validates a set of credentials for a SFTP login aganist Pterodactyl Panel and returns
|
||||||
|
// the server's UUID if the credentials were valid.
|
||||||
|
func validateCredentials(c sftp_server.AuthenticationRequest) (*sftp_server.AuthenticationResponse, error) {
|
||||||
|
log.WithFields(log.Fields{"subsystem": "sftp", "username": c.User}).Debug("validating credentials for SFTP connection")
|
||||||
|
|
||||||
|
f := log.Fields{
|
||||||
|
"subsystem": "sftp",
|
||||||
|
"username": c.User,
|
||||||
|
"ip": c.IP,
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the username doesn't meet the expected format that the Panel would even recognize just go ahead
|
||||||
|
// and bail out of the process here to avoid accidentially brute forcing the panel if a bot decides
|
||||||
|
// to connect to spam username attempts.
|
||||||
|
if !validUsernameRegexp.MatchString(c.User) {
|
||||||
|
log.WithFields(f).Warn("failed to validate user credentials (invalid format)")
|
||||||
|
|
||||||
|
return nil, new(sftp_server.InvalidCredentialsError)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := api.NewRequester().ValidateSftpCredentials(c)
|
||||||
|
if err != nil {
|
||||||
|
if sftp_server.IsInvalidCredentialsError(err) {
|
||||||
|
log.WithFields(f).Warn("failed to validate user credentials (invalid username or password)")
|
||||||
|
} else {
|
||||||
|
log.WithFields(f).Error("encountered an error while trying to validate user credentials")
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s := server.GetServers().Find(func(server *server.Server) bool {
|
||||||
|
return server.Id() == resp.Server
|
||||||
|
})
|
||||||
|
|
||||||
|
if s == nil {
|
||||||
|
return resp, errors.New("no matching server with UUID found")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Log().WithFields(f).Debug("credentials successfully validated and matched user to server instance")
|
||||||
|
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|||||||
97
sftp/sftp.go
97
sftp/sftp.go
@@ -1,97 +0,0 @@
|
|||||||
package sftp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/apex/log"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/pterodactyl/wings/api"
|
|
||||||
"github.com/pterodactyl/wings/config"
|
|
||||||
"github.com/pterodactyl/wings/server"
|
|
||||||
)
|
|
||||||
|
|
||||||
var noMatchingServerError = errors.New("no matching server with that UUID was found")
|
|
||||||
|
|
||||||
func Initialize(config config.SystemConfiguration) error {
|
|
||||||
s := &Server{
|
|
||||||
User: User{
|
|
||||||
Uid: config.User.Uid,
|
|
||||||
Gid: config.User.Gid,
|
|
||||||
},
|
|
||||||
Settings: Settings{
|
|
||||||
BasePath: config.Data,
|
|
||||||
ReadOnly: config.Sftp.ReadOnly,
|
|
||||||
BindAddress: config.Sftp.Address,
|
|
||||||
BindPort: config.Sftp.Port,
|
|
||||||
},
|
|
||||||
CredentialValidator: validateCredentials,
|
|
||||||
PathValidator: validatePath,
|
|
||||||
DiskSpaceValidator: validateDiskSpace,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := New(s); err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the SFTP server in a background thread since this is
|
|
||||||
// a long running operation.
|
|
||||||
go func(s *Server) {
|
|
||||||
if err := s.Initialize(); err != nil {
|
|
||||||
log.WithField("subsystem", "sftp").WithField("error", errors.WithStack(err)).Error("failed to initialize SFTP subsystem")
|
|
||||||
}
|
|
||||||
}(s)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validatePath(fs FileSystem, p string) (string, error) {
|
|
||||||
s := server.GetServers().Find(func(server *server.Server) bool {
|
|
||||||
return server.Id() == fs.UUID
|
|
||||||
})
|
|
||||||
|
|
||||||
if s == nil {
|
|
||||||
return "", noMatchingServerError
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.Filesystem.SafePath(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateDiskSpace(fs FileSystem) bool {
|
|
||||||
s := server.GetServers().Find(func(server *server.Server) bool {
|
|
||||||
return server.Id() == fs.UUID
|
|
||||||
})
|
|
||||||
|
|
||||||
if s == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.Filesystem.HasSpaceAvailable(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validates a set of credentials for a SFTP login against Pterodactyl Panel and returns
|
|
||||||
// the server's UUID if the credentials were valid.
|
|
||||||
func validateCredentials(c api.SftpAuthRequest) (*api.SftpAuthResponse, error) {
|
|
||||||
f := log.Fields{"subsystem": "sftp", "username": c.User, "ip": c.IP}
|
|
||||||
|
|
||||||
log.WithFields(f).Debug("validating credentials for SFTP connection")
|
|
||||||
resp, err := api.NewRequester().ValidateSftpCredentials(c)
|
|
||||||
if err != nil {
|
|
||||||
if api.IsInvalidCredentialsError(err) {
|
|
||||||
log.WithFields(f).Warn("failed to validate user credentials (invalid username or password)")
|
|
||||||
} else {
|
|
||||||
log.WithFields(f).Error("encountered an error while trying to validate user credentials")
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
|
|
||||||
s := server.GetServers().Find(func(server *server.Server) bool {
|
|
||||||
return server.Id() == resp.Server
|
|
||||||
})
|
|
||||||
|
|
||||||
if s == nil {
|
|
||||||
return resp, noMatchingServerError
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Log().WithFields(f).Debug("credentials successfully validated and matched user to server instance")
|
|
||||||
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{{.LogDirectory}}/wings.log {
|
|
||||||
size 10M
|
|
||||||
compress
|
|
||||||
delaycompress
|
|
||||||
dateext
|
|
||||||
maxage 7
|
|
||||||
missingok
|
|
||||||
notifempty
|
|
||||||
create 0640 {{.User.Uid}} {{.User.Gid}}
|
|
||||||
postrotate
|
|
||||||
killall -SIGHUP wings
|
|
||||||
endscript
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user