Very, very basic server installation process
This commit is contained in:
parent
853d215b1d
commit
6ef2773c01
|
@ -41,6 +41,14 @@ type ProcessConfiguration struct {
|
||||||
ConfigurationFiles []parser.ConfigurationFile `json:"configs"`
|
ConfigurationFiles []parser.ConfigurationFile `json:"configs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
type InstallationScript struct {
|
||||||
|
ContainerImage string `json:"container_image"`
|
||||||
|
Entrypoint string `json:"entrypoint"`
|
||||||
|
Script string `json:"script"`
|
||||||
|
}
|
||||||
|
|
||||||
// Fetches the server configuration and returns the struct for it.
|
// Fetches the server configuration and returns the struct for it.
|
||||||
func (r *PanelRequest) GetServerConfiguration(uuid string) (*ServerConfigurationResponse, *RequestError, error) {
|
func (r *PanelRequest) GetServerConfiguration(uuid string) (*ServerConfigurationResponse, *RequestError, error) {
|
||||||
resp, err := r.Get(fmt.Sprintf("/servers/%s", uuid))
|
resp, err := r.Get(fmt.Sprintf("/servers/%s", uuid))
|
||||||
|
@ -64,3 +72,28 @@ func (r *PanelRequest) GetServerConfiguration(uuid string) (*ServerConfiguration
|
||||||
|
|
||||||
return res, nil, nil
|
return res, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetches installation information for the server process.
|
||||||
|
func (r *PanelRequest) GetInstallationScript(uuid string) (InstallationScript, *RequestError, error) {
|
||||||
|
res := InstallationScript{}
|
||||||
|
|
||||||
|
resp, err := r.Get(fmt.Sprintf("/servers/%s/install", uuid))
|
||||||
|
if err != nil {
|
||||||
|
return res, nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
r.Response = resp
|
||||||
|
|
||||||
|
if r.HasError() {
|
||||||
|
return res, r.Error(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b, _ := r.ReadBody()
|
||||||
|
|
||||||
|
if err := json.Unmarshal(b, &res); err != nil {
|
||||||
|
return res, nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil, nil
|
||||||
|
}
|
||||||
|
|
17
http.go
17
http.go
|
@ -420,6 +420,20 @@ func (rt *Router) routeServerSendCommand(w http.ResponseWriter, r *http.Request,
|
||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rt *Router) routeServerInstall(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||||
|
s := rt.GetServer(ps.ByName("server"))
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
if err := s.Install(); err != nil {
|
||||||
|
zap.S().Errorw("failed to install server", zap.String("server", s.Uuid), zap.Error(err))
|
||||||
|
|
||||||
|
http.Error(w, "failed to install server", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
func (rt *Router) routeServerUpdate(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
func (rt *Router) routeServerUpdate(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||||
s := rt.GetServer(ps.ByName("server"))
|
s := rt.GetServer(ps.ByName("server"))
|
||||||
defer r.Body.Close()
|
defer r.Body.Close()
|
||||||
|
@ -545,7 +559,8 @@ func (rt *Router) ConfigureRouter() *httprouter.Router {
|
||||||
router.GET("/api/servers/:server/files/contents", rt.AuthenticateRequest(rt.routeServerFileRead))
|
router.GET("/api/servers/:server/files/contents", rt.AuthenticateRequest(rt.routeServerFileRead))
|
||||||
router.GET("/api/servers/:server/files/list-directory", rt.AuthenticateRequest(rt.routeServerListDirectory))
|
router.GET("/api/servers/:server/files/list-directory", rt.AuthenticateRequest(rt.routeServerListDirectory))
|
||||||
router.PUT("/api/servers/:server/files/rename", rt.AuthenticateRequest(rt.routeServerRenameFile))
|
router.PUT("/api/servers/:server/files/rename", rt.AuthenticateRequest(rt.routeServerRenameFile))
|
||||||
router.POST("/api/servers", rt.AuthenticateToken(rt.routeCreateServer));
|
router.POST("/api/servers", rt.AuthenticateToken(rt.routeCreateServer))
|
||||||
|
router.POST("/api/servers/:server/install", rt.AuthenticateRequest(rt.routeServerInstall))
|
||||||
router.POST("/api/servers/:server/files/copy", rt.AuthenticateRequest(rt.routeServerCopyFile))
|
router.POST("/api/servers/:server/files/copy", rt.AuthenticateRequest(rt.routeServerCopyFile))
|
||||||
router.POST("/api/servers/:server/files/write", rt.AuthenticateRequest(rt.routeServerWriteFile))
|
router.POST("/api/servers/:server/files/write", rt.AuthenticateRequest(rt.routeServerWriteFile))
|
||||||
router.POST("/api/servers/:server/files/create-directory", rt.AuthenticateRequest(rt.routeServerCreateDirectory))
|
router.POST("/api/servers/:server/files/create-directory", rt.AuthenticateRequest(rt.routeServerCreateDirectory))
|
||||||
|
|
|
@ -545,6 +545,7 @@ func (d *DockerEnvironment) Create() error {
|
||||||
|
|
||||||
Labels: map[string]string{
|
Labels: map[string]string{
|
||||||
"Service": "Pterodactyl",
|
"Service": "Pterodactyl",
|
||||||
|
"ContainerType": "server_process",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
263
server/install.go
Normal file
263
server/install.go
Normal file
|
@ -0,0 +1,263 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"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/pkg/errors"
|
||||||
|
"github.com/pterodactyl/wings/api"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Server) Install() error {
|
||||||
|
script, rerr, err := api.NewRequester().GetInstallationScript(s.Uuid)
|
||||||
|
if err != nil || rerr != nil {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New(rerr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := NewInstallationProcess(s, &script)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.Execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
type InstallationProcess struct {
|
||||||
|
Server *Server
|
||||||
|
Script *api.InstallationScript
|
||||||
|
|
||||||
|
client *client.Client
|
||||||
|
mutex *sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates a new installation process struct that will be used to create containers,
|
||||||
|
// and otherwise perform installation commands for a server.
|
||||||
|
func NewInstallationProcess(s *Server, script *api.InstallationScript) (*InstallationProcess, error) {
|
||||||
|
proc := &InstallationProcess{
|
||||||
|
Script: script,
|
||||||
|
Server: s,
|
||||||
|
mutex: &sync.Mutex{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if c, err := client.NewClientWithOpts(client.FromEnv); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
} else {
|
||||||
|
proc.client = c
|
||||||
|
}
|
||||||
|
|
||||||
|
return proc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
func (ip *InstallationProcess) writeScriptToDisk() (string, error) {
|
||||||
|
d, err := ioutil.TempDir("", "pterodactyl")
|
||||||
|
if err != nil {
|
||||||
|
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()
|
||||||
|
|
||||||
|
w := bufio.NewWriter(f)
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(bytes.NewReader([]byte(ip.Script.Script)))
|
||||||
|
for scanner.Scan() {
|
||||||
|
w.WriteString(scanner.Text()+"\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return "", errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Flush()
|
||||||
|
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pulls the docker image to be used for the installation container.
|
||||||
|
func (ip *InstallationProcess) pullInstallationImage() error {
|
||||||
|
r, err := ip.client.ImagePull(context.Background(), ip.Script.ContainerImage, types.ImagePullOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy to stdout until we hit an EOF or other fatal error which would
|
||||||
|
// require exiting.
|
||||||
|
if _, err := io.Copy(os.Stdout, r); err != nil && err != io.EOF {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// manner, if either one fails the error is returned.
|
||||||
|
func (ip *InstallationProcess) beforeExecute() (string, error) {
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
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 {
|
||||||
|
e = append(e, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
opts := types.ContainerRemoveOptions{
|
||||||
|
RemoveVolumes: true,
|
||||||
|
Force: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ip.client.ContainerRemove(context.Background(), ip.Server.Uuid+"_installer", opts); err != nil {
|
||||||
|
if !client.IsErrNotFound(err) {
|
||||||
|
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 fileName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Executes the installation process inside a specially created docker container.
|
||||||
|
func (ip *InstallationProcess) Execute() error {
|
||||||
|
installScriptPath, err := ip.beforeExecute()
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
zap.S().Debugw(
|
||||||
|
"creating server installer container",
|
||||||
|
zap.String("server", ip.Server.Uuid),
|
||||||
|
zap.String("script_path", installScriptPath+"/install.sh"),
|
||||||
|
)
|
||||||
|
|
||||||
|
conf := &container.Config{
|
||||||
|
Hostname: "installer",
|
||||||
|
AttachStdout: true,
|
||||||
|
AttachStderr: true,
|
||||||
|
AttachStdin: true,
|
||||||
|
OpenStdin: true,
|
||||||
|
Tty: true,
|
||||||
|
Cmd: []string{ip.Script.Entrypoint, "./mnt/install/install.sh"},
|
||||||
|
Image: ip.Script.ContainerImage,
|
||||||
|
Env: ip.Server.GetEnvironmentVariables(),
|
||||||
|
Labels: map[string]string{
|
||||||
|
"Service": "Pterodactyl",
|
||||||
|
"ContainerType": "server_installer",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
hostConf := &container.HostConfig{
|
||||||
|
Mounts: []mount.Mount{
|
||||||
|
{
|
||||||
|
Target: "/mnt/server",
|
||||||
|
Source: ip.Server.Filesystem.Path(),
|
||||||
|
Type: mount.TypeBind,
|
||||||
|
ReadOnly: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Target: "/mnt/install",
|
||||||
|
Source: installScriptPath,
|
||||||
|
Type: mount.TypeBind,
|
||||||
|
ReadOnly: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Tmpfs: map[string]string{
|
||||||
|
"/tmp": "rw,exec,nosuid,size=50M",
|
||||||
|
},
|
||||||
|
DNS: []string{"1.1.1.1", "8.8.8.8"},
|
||||||
|
LogConfig: container.LogConfig{
|
||||||
|
Type: "local",
|
||||||
|
Config: map[string]string{
|
||||||
|
"max-size": "20m",
|
||||||
|
"max-file": "1",
|
||||||
|
"compress": "false",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Privileged: true,
|
||||||
|
NetworkMode: "pterodactyl_nw",
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.S().Infow("creating installer container for server process", zap.String("server", ip.Server.Uuid))
|
||||||
|
r, err := ip.client.ContainerCreate(ctx, conf, hostConf, nil, ip.Server.Uuid+"_installer")
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
zap.S().Infow("running installation process for server", zap.String("server", ip.Server.Uuid))
|
||||||
|
if err := ip.client.ContainerStart(ctx, r.ID, types.ContainerStartOptions{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
stream, err := ip.client.ContainerAttach(ctx, r.ID, types.ContainerAttachOptions{
|
||||||
|
Stdout: true,
|
||||||
|
Stderr: true,
|
||||||
|
Stream: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer stream.Close()
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
io.Copy(os.Stdout, stream.Reader)
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
zap.S().Infow("completed installation process", zap.String("server", ip.Server.Uuid))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -247,6 +247,33 @@ func FromConfiguration(data []byte, cfg *config.SystemConfiguration) (*Server, e
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns all of the environment variables that should be assigned to a running
|
||||||
|
// server instance.
|
||||||
|
func (s *Server) GetEnvironmentVariables() []string {
|
||||||
|
zone, _ := time.Now().In(time.Local).Zone()
|
||||||
|
|
||||||
|
var out = []string{
|
||||||
|
fmt.Sprintf("TZ=%s", zone),
|
||||||
|
fmt.Sprintf("STARTUP=%s", s.Invocation),
|
||||||
|
fmt.Sprintf("SERVER_MEMORY=%d", s.Build.MemoryLimit),
|
||||||
|
fmt.Sprintf("SERVER_IP=%s", s.Allocations.DefaultMapping.Ip),
|
||||||
|
fmt.Sprintf("SERVER_PORT=%d", s.Allocations.DefaultMapping.Port),
|
||||||
|
}
|
||||||
|
|
||||||
|
eloop:
|
||||||
|
for k, v := range s.EnvVars {
|
||||||
|
for _, e := range out {
|
||||||
|
if strings.HasPrefix(e, strings.ToUpper(k)) {
|
||||||
|
continue eloop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out = append(out, fmt.Sprintf("%s=%s", strings.ToUpper(k), v))
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// Syncs the state of the server on the Panel with Wings. This ensures that we're always
|
// Syncs the state of the server on the Panel with Wings. This ensures that we're always
|
||||||
// using the state of the server from the Panel and allows us to not require successful
|
// using the state of the server from the Panel and allows us to not require successful
|
||||||
// API calls to Wings to do things.
|
// API calls to Wings to do things.
|
||||||
|
|
Loading…
Reference in New Issue
Block a user