commit f098d5f17019ba92a564f8b78e0f0774c2c77328 Author: vxclutch Date: Wed Jun 24 13:44:39 2026 -0400 Inital commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d175a7b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +mc-tool diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..f257828 --- /dev/null +++ b/config/config.go @@ -0,0 +1,30 @@ +package config + +import ( + "text/template" +) + +var Cfg Config = Config{ + Name: "minecraft-server", + Version: "1.21.1", + Type: "VANILLA", + Ram: "2G", + MOTD: "This server was created using mc-tool", + RestartPolicy: "never", + Ports: []string{"25565:25565"}, +} +var Data Resources + +type Config struct { + Name string + Version string + Type string + Ram string + MOTD string + RestartPolicy string + Ports []string +} + +type Resources struct { + DockerCompose *template.Template +} diff --git a/docker/docker.go b/docker/docker.go new file mode 100644 index 0000000..56472f4 --- /dev/null +++ b/docker/docker.go @@ -0,0 +1,67 @@ +package docker + +import ( + "fmt" + "log" + "strings" +) + +type Port struct { + In string + Out string +} + +type ServerType int + +const ( + FORGE ServerType = iota + FABRIC + NEOFORGE + VANILLA +) + +func (p Port) String() string { + return fmt.Sprintf("%s:%s", p.In, p.Out) +} + +func (t ServerType) String() string { + switch t { + case FORGE: + return "FORGE" + case NEOFORGE: + return "NEOFORGE" + case VANILLA: + return "VANILLA" + } + log.Panicln("unreachable") + return "" +} + +func PortFromString(port string) Port { + parts := strings.Split(port, ":") + return Port{ + In: parts[0], + Out: parts[1], + } +} + +func PortsFromStrings(ports []string) (r []Port) { + for _, port := range ports { + r = append(r, PortFromString(port)) + } + return +} + +func TypeFromString(in string) ServerType { + switch in { + case "FORGE": + return FORGE + case "NEOFORGE": + return NEOFORGE + case "VANILLA": + return VANILLA + default: + log.Fatalf("unknown server type \"%s\"", in) + } + return VANILLA +} diff --git a/docker/docker_compose.go b/docker/docker_compose.go new file mode 100644 index 0000000..7e0ed2f --- /dev/null +++ b/docker/docker_compose.go @@ -0,0 +1,39 @@ +package docker + +import ( + "log" + "mc-tool/config" + "strings" +) + +type Dockercompose struct { + Name string + Ports []Port + Version string + Type string + Ram string + MOTD string + RestartPolicy string +} + +func NewCompose(name string) Dockercompose { + return Dockercompose{ + Name: name, + Ports: PortsFromStrings(config.Cfg.Ports), + Version: config.Cfg.Version, + Type: TypeFromString(config.Cfg.Type).String(), + Ram: config.Cfg.Ram, + MOTD: config.Cfg.MOTD, + RestartPolicy: config.Cfg.RestartPolicy, + } +} + +func (dc Dockercompose) String() string { + b := new(strings.Builder) + + err := config.Data.DockerCompose.ExecuteTemplate(b, "compose", dc) + if err != nil { + log.Fatal(err) + } + return b.String() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c6a42a4 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module mc-tool + +go 1.26.4 + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/pflag v1.0.10 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ef5d78d --- /dev/null +++ b/go.sum @@ -0,0 +1,11 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/main.go b/main.go new file mode 100644 index 0000000..e5be196 --- /dev/null +++ b/main.go @@ -0,0 +1,90 @@ +package main + +import ( + _ "embed" + "log" + "mc-tool/config" + "mc-tool/server" + "os" + "text/template" + + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "mc-tool [command]", + Short: "Minecraft Management Tool", + Long: "mc-tool simplifies common server administration actions", + Args: cobra.MinimumNArgs(1), +} + +var createCmd = &cobra.Command{ + Use: "create", + Short: "Creates a new minecraft server", + Run: server.New, + Args: cobra.ExactArgs(1), +} + +var startCmd = &cobra.Command{ + Use: "start", + Short: "Start the minecraft server", + Run: server.Start, + Args: cobra.ExactArgs(1), +} + +var stopCmd = &cobra.Command{ + Use: "stop", + Short: "Stop the minecraft server", + Run: server.Stop, + Args: cobra.ExactArgs(1), +} + +var restartCmd = &cobra.Command{ + Use: "restart", + Short: "Restart the minecraft server", + Run: server.Restart, + Args: cobra.ExactArgs(1), +} + +var shellCmd = &cobra.Command{ + Use: "shell", + Short: "Access RCON shell", + Run: server.Shell, + Args: cobra.ExactArgs(1), +} + +//go:embed templates/docker-compose.yml +var dockerCompose string + +func init() { + createCmd.Flags().StringVarP(&config.Cfg.Name, "name", "n", "minecraft-server", "set the name of the server") + createCmd.Flags().StringVarP(&config.Cfg.Version, "version", "v", "1.21.1", "set the version of the server") + createCmd.Flags().StringVarP(&config.Cfg.Type, "type", "t", "VANILLA", "set the type of the server") + createCmd.Flags().StringVarP(&config.Cfg.Ram, "ram", "r", "2G", "set the memory limit of the server") + createCmd.Flags().StringVarP(&config.Cfg.MOTD, "motd", "m", "This server was created using mc-tool", "set the message of the day for the server") + createCmd.Flags().StringVarP(&config.Cfg.RestartPolicy, "restart", "R", "never", "what to do when the server fails or host turns off") + createCmd.Flags().FuncP("port", "p", "set additional ports for the container to expose", func(s string) error { + config.Cfg.Ports = append(config.Cfg.Ports, s) + return nil + }) + + rootCmd.AddCommand(createCmd) + rootCmd.AddCommand(startCmd) + rootCmd.AddCommand(stopCmd) + rootCmd.AddCommand(restartCmd) + rootCmd.AddCommand(shellCmd) + + t, err := template.New("compose").Parse(dockerCompose) + if err != nil { + log.Fatal(err) + } + config.Data.DockerCompose = t +} + +func main() { + log.SetFlags(0) + if err := rootCmd.Execute(); err != nil { + log.Fatal(err) + os.Exit(1) + } +} diff --git a/server/container.go b/server/container.go new file mode 100644 index 0000000..07bc4db --- /dev/null +++ b/server/container.go @@ -0,0 +1,37 @@ +package server + +import ( + "bufio" + "log" + "os" + "strings" +) + +func getServerName(dir string) string { + if err := os.Chdir(dir); err != nil { + log.Fatal(err) + } + + file, err := os.Open("docker-compose.yml") + if err != nil { + log.Fatal(err) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + + name := dir + for scanner.Scan() { + txt := scanner.Text() + txt = strings.TrimSpace(txt) + s, found := strings.CutPrefix(txt, "container_name: ") + if found { + name = s + } + } + + if err := scanner.Err(); err != nil { + log.Fatal(err) + } + return name +} diff --git a/server/new.go b/server/new.go new file mode 100644 index 0000000..aa6a42c --- /dev/null +++ b/server/new.go @@ -0,0 +1,47 @@ +package server + +import ( + "log" + "mc-tool/docker" + "os" + "path/filepath" + + "github.com/spf13/cobra" +) + +func New(cmd *cobra.Command, args []string) { + dir := args[0] + if err := os.Mkdir(dir, 0755); err != nil { + log.Fatal(err) + } + + if err := os.Chdir(dir); err != nil { + log.Fatal(err) + } + + compose := docker.NewCompose(dir) + + if compose.Type != docker.VANILLA.String() { + if err := os.MkdirAll("data/mods", 0755); err != nil { + log.Fatal(err) + } + if err := os.MkdirAll("data/config", 0755); err != nil { + log.Fatal(err) + } + } + + if err := os.MkdirAll("data/world", 0755); err != nil { + log.Fatal(err) + } + + if err := writeFile("docker-compose.yml", compose.String()); err != nil { + log.Fatal(err) + } +} + +func writeFile(path string, data string) error { + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + return os.WriteFile(path, []byte(data), 0644) +} diff --git a/server/restart.go b/server/restart.go new file mode 100644 index 0000000..32d7534 --- /dev/null +++ b/server/restart.go @@ -0,0 +1,21 @@ +package server + +import ( + "log" + "os" + "os/exec" + + "github.com/spf13/cobra" +) + +func Restart(cmd *cobra.Command, args []string) { + dir := args[0] + if err := os.Chdir(dir); err != nil { + log.Fatal(err) + } + + start_cmd := exec.Command("docker", "compose", "restart") + if err := start_cmd.Run(); err != nil { + log.Fatal(err) + } +} diff --git a/server/shell.go b/server/shell.go new file mode 100644 index 0000000..8ce2310 --- /dev/null +++ b/server/shell.go @@ -0,0 +1,31 @@ +package server + +import ( + "log" + "os/exec" + "strings" + + "github.com/spf13/cobra" +) + +func Shell(cmd *cobra.Command, args []string) { + dir := args[0] + + name := getServerName(dir) + + start_cmd := exec.Command("docker", "exec", name, "rcon-cli") + if err := start_cmd.Run(); err != nil { + log.Fatal(err) + } +} + +func Say(cmd *cobra.Command, args []string) { + dir := args[0] + + name := getServerName(dir) + + start_cmd := exec.Command("docker", "exec", name, "rcon-cli", strings.Join(args[1:], " ")) + if err := start_cmd.Run(); err != nil { + log.Fatal(err) + } +} diff --git a/server/start.go b/server/start.go new file mode 100644 index 0000000..40efadb --- /dev/null +++ b/server/start.go @@ -0,0 +1,21 @@ +package server + +import ( + "log" + "os" + "os/exec" + + "github.com/spf13/cobra" +) + +func Start(cmd *cobra.Command, args []string) { + dir := args[0] + if err := os.Chdir(dir); err != nil { + log.Fatal(err) + } + + start_cmd := exec.Command("docker", "compose", "up", "-d") + if err := start_cmd.Run(); err != nil { + log.Fatal(err) + } +} diff --git a/server/status.go b/server/status.go new file mode 100644 index 0000000..426956b --- /dev/null +++ b/server/status.go @@ -0,0 +1,28 @@ +package server + +import ( + "bytes" + "fmt" + "log" + "os/exec" + + "github.com/spf13/cobra" +) + +func Status(cmd *cobra.Command, args []string) { + dir := args[0] + + name := getServerName(dir) + + start_cmd := exec.Command("docker", "exec", name, "rcon-cli", "list") + var outb, errb bytes.Buffer + + start_cmd.Stdout = &outb + start_cmd.Stderr = &errb + + if err := start_cmd.Run(); err != nil { + log.Fatal(err) + } + + fmt.Println(outb.String()) +} diff --git a/server/stop.go b/server/stop.go new file mode 100644 index 0000000..204dbcc --- /dev/null +++ b/server/stop.go @@ -0,0 +1,21 @@ +package server + +import ( + "log" + "os" + "os/exec" + + "github.com/spf13/cobra" +) + +func Stop(cmd *cobra.Command, args []string) { + dir := args[0] + if err := os.Chdir(dir); err != nil { + log.Fatal(err) + } + + start_cmd := exec.Command("docker", "compose", "down") + if err := start_cmd.Run(); err != nil { + log.Fatal(err) + } +} diff --git a/templates/docker-compose.yml b/templates/docker-compose.yml new file mode 100644 index 0000000..9d4d070 --- /dev/null +++ b/templates/docker-compose.yml @@ -0,0 +1,18 @@ +service: + mc: + image: itzg/minecraft-server:latest + container_name: {{.Name}} + tty: true + stdin_open: true + ports: + {{range .Ports}}- "{{.In}}:{{.Out}}" + {{end}} + environment: + EULA: "TRUE" + VERSION: "{{.Version}}" + TYPE: {{.Type}} + MEMORY: "{{.Ram}}" + MOTD: "{{.MOTD}}" + volumes: + - ./data:/data + restart: "{{.RestartPolicy}}"