diff --git a/generate-scrolls.go b/generate-scrolls.go index 6f6a107f..4d980658 100644 --- a/generate-scrolls.go +++ b/generate-scrolls.go @@ -237,6 +237,9 @@ func ParsePorts(spec string) []PortSpec { if protocol == "" { protocol = "tcp" } + if port == "0" { + port = "" + } ports = append(ports, PortSpec{ Name: name, Port: port, diff --git a/generate-scrolls_test.go b/generate-scrolls_test.go new file mode 100644 index 00000000..5d8f9eeb --- /dev/null +++ b/generate-scrolls_test.go @@ -0,0 +1,127 @@ +package main + +import ( + "os" + "testing" + + "gopkg.in/yaml.v3" +) + +func TestParsePortsSupportsDynamicPorts(t *testing.T) { + ports := ParsePorts("main=/udp;query=0/udp;rcon;fixed=27015;zero=0") + if len(ports) != 5 { + t.Fatalf("ports = %#v", ports) + } + if ports[0].Name != "main" || ports[0].Port != "" || ports[0].Protocol != "udp" { + t.Fatalf("main = %#v", ports[0]) + } + if ports[1].Name != "query" || ports[1].Port != "" || ports[1].Protocol != "udp" { + t.Fatalf("query = %#v", ports[1]) + } + if ports[2].Name != "rcon" || ports[2].Port != "" || ports[2].Protocol != "tcp" { + t.Fatalf("rcon = %#v", ports[2]) + } + if ports[3].Port != "27015" { + t.Fatalf("fixed = %#v", ports[3]) + } + if ports[4].Name != "zero" || ports[4].Port != "" || ports[4].Protocol != "tcp" { + t.Fatalf("zero = %#v", ports[4]) + } +} + +func TestGeneratedARKHandsRCONToRuntimeServer(t *testing.T) { + scroll := readGeneratedScroll(t, "scrolls/lgsm/arkserver/scroll.yaml") + console := scroll.Commands["console"] + for _, procedureID := range []string{"coldstart", "start"} { + procedure := findGeneratedProcedure(t, console.Procedures, procedureID) + if !hasGeneratedExpectedPort(procedure.ExpectedPorts, "rcon") { + t.Fatalf("%s expectedPorts = %#v, want rcon", procedureID, procedure.ExpectedPorts) + } + } +} + +func TestGeneratedSharedPortsRemainConcrete(t *testing.T) { + tests := []struct { + path string + want map[string]int + }{ + { + path: "scrolls/lgsm/cs2server/scroll.yaml", + want: map[string]int{"main": 27015, "rcon": 27015}, + }, + { + path: "scrolls/lgsm/pzserver/scroll.yaml", + want: map[string]int{"main": 16261, "main2": 16262, "maintcp": 16261}, + }, + } + for _, test := range tests { + t.Run(test.path, func(t *testing.T) { + scroll := readGeneratedScroll(t, test.path) + got := make(map[string]int, len(scroll.Ports)) + for _, port := range scroll.Ports { + got[port.Name] = port.Port + } + for name, want := range test.want { + if got[name] != want { + t.Fatalf("port %s = %d, want %d", name, got[name], want) + } + } + }) + } +} + +type generatedScroll struct { + Ports []generatedPort `yaml:"ports"` + Commands map[string]generatedCommand `yaml:"commands"` +} + +type generatedPort struct { + Name string `yaml:"name"` + Port int `yaml:"port"` +} + +type generatedCommand struct { + Procedures []generatedProcedure `yaml:"procedures"` +} + +type generatedProcedure struct { + ID string `yaml:"id"` + ExpectedPorts []generatedExpectedPort `yaml:"expectedPorts"` +} + +type generatedExpectedPort struct { + Name string `yaml:"name"` +} + +func readGeneratedScroll(t *testing.T, path string) generatedScroll { + t.Helper() + content, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + var scroll generatedScroll + if err := yaml.Unmarshal(content, &scroll); err != nil { + t.Fatal(err) + } + return scroll +} + +func findGeneratedProcedure(t *testing.T, procedures []generatedProcedure, id string) generatedProcedure { + t.Helper() + for _, procedure := range procedures { + if procedure.ID == id { + return procedure + } + } + t.Fatalf("procedure %q not found", id) + return generatedProcedure{} +} + +func hasGeneratedExpectedPort(ports []generatedExpectedPort, name string) bool { + for _, port := range ports { + if port.Name == name { + return true + } + } + return false +} diff --git a/scripts/prebuild/main.go b/scripts/prebuild/main.go index 9b2cdaba..e351ea5d 100644 --- a/scripts/prebuild/main.go +++ b/scripts/prebuild/main.go @@ -641,16 +641,16 @@ func allSpecs() []prebuildSpec { steamImage := getenv("DRUID_STEAM_RUNTIME_IMAGE", "artifacts.druid.gg/druid-team/druid:v0.1.249-steamcmd") specs := []prebuildSpec{ {Target: "pwserver", Artifact: "artifacts.druid.gg/druid-team/scroll-lgsm:pwserver-prebuild", Source: "./scrolls/lgsm/pwserver", Image: steamImage, Ports: []string{"main=8211/udp", "rcon=25575"}, MinDisk: "7Gi", MinRAM: "2Gi", MinCPU: "0.5", Category: "palworld", Smart: true, PackMeta: true}, - {Target: "arkserver", Artifact: "artifacts.druid.gg/druid-team/scroll-lgsm:arkserver-prebuild", Source: "./scrolls/lgsm/arkserver", Image: steamImage, Ports: []string{"main=7777/udp", "query=27015/udp", "rcon=27020"}, MinDisk: "25Gi", MinRAM: "7Gi", MinCPU: "0.5", Category: "ark", Smart: true, PackMeta: true}, - {Target: "dayzserver", Artifact: "artifacts.druid.gg/druid-team/scroll-lgsm:dayzserver-prebuild", Source: "./scrolls/lgsm/dayzserver", Image: steamImage, Ports: []string{"main=2302/udp", "battle-eye=2304/udp", "query=27016/udp"}, MinDisk: "7Gi", MinRAM: "5Gi", MinCPU: "1", Category: "dayz", PackMeta: true, RequiredEnv: []string{"STEAM_USER", "STEAM_PASS"}}, - {Target: "untserver", Artifact: "artifacts.druid.gg/druid-team/scroll-lgsm:untserver-prebuild", Source: "./scrolls/lgsm/untserver", Image: steamImage, Ports: []string{"main=27015/udp", "mainv6=27016"}, MinDisk: "7Gi", MinRAM: "1Gi", MinCPU: "0.5", Category: "unturned", Smart: true, PackMeta: true}, + {Target: "arkserver", Artifact: "artifacts.druid.gg/druid-team/scroll-lgsm:arkserver-prebuild", Source: "./scrolls/lgsm/arkserver", Image: steamImage, Ports: []string{"main=/udp", "query=/udp", "rcon"}, MinDisk: "25Gi", MinRAM: "7Gi", MinCPU: "0.5", Category: "ark", Smart: true, PackMeta: true}, + {Target: "dayzserver", Artifact: "artifacts.druid.gg/druid-team/scroll-lgsm:dayzserver-prebuild", Source: "./scrolls/lgsm/dayzserver", Image: steamImage, Ports: []string{"main=/udp", "battle-eye=2304/udp", "query=27016/udp"}, MinDisk: "7Gi", MinRAM: "5Gi", MinCPU: "1", Category: "dayz", PackMeta: true, RequiredEnv: []string{"STEAM_USER", "STEAM_PASS"}}, + {Target: "untserver", Artifact: "artifacts.druid.gg/druid-team/scroll-lgsm:untserver-prebuild", Source: "./scrolls/lgsm/untserver", Image: steamImage, Ports: []string{"main=/udp", "mainv6=27016"}, MinDisk: "7Gi", MinRAM: "1Gi", MinCPU: "0.5", Category: "unturned", Smart: true, PackMeta: true}, {Target: "sdtdserver", Artifact: "artifacts.druid.gg/druid-team/scroll-lgsm:sdtdserver-prebuild", Source: "./scrolls/lgsm/sdtdserver", Image: steamImage, Ports: []string{"query=26900/udp", "main=26900/udp", "main2=26902/udp", "maintcp=26900"}, MinDisk: "20Gi", MinRAM: "2Gi", MinCPU: "0.5", Category: "7days", PackMeta: true}, - {Target: "gmodserver", Artifact: "artifacts.druid.gg/druid-team/scroll-lgsm:gmodserver-prebuild", Source: "./scrolls/lgsm/gmodserver", Image: steamImage, Ports: []string{"query=27005/udp", "main=27015/udp", "sourcetv=27020/udp", "steam=27015"}, MinDisk: "8Gi", MinRAM: "512Mi", MinCPU: "0.25", Category: "gmod", Smart: true, PackMeta: true}, + {Target: "gmodserver", Artifact: "artifacts.druid.gg/druid-team/scroll-lgsm:gmodserver-prebuild", Source: "./scrolls/lgsm/gmodserver", Image: steamImage, Ports: []string{"query=27005/udp", "main=/udp", "sourcetv=27020/udp", "steam=27015"}, MinDisk: "8Gi", MinRAM: "512Mi", MinCPU: "0.25", Category: "gmod", Smart: true, PackMeta: true}, {Target: "cs2server", Artifact: "artifacts.druid.gg/druid-team/scroll-lgsm:cs2server-prebuild", Source: "./scrolls/lgsm/cs2server", Image: steamImage, Ports: []string{"main=27015/udp", "rcon=27015"}, MinDisk: "70Gi", BuildDisk: "95Gi", MinRAM: "1Gi", MinCPU: "0.5", Category: "cs2", Smart: true, PackMeta: true}, {Target: "pzserver", Artifact: "artifacts.druid.gg/druid-team/scroll-lgsm:pzserver-prebuild", Source: "./scrolls/lgsm/pzserver", Image: steamImage, Ports: []string{"main=16261/udp", "main2=16262/udp", "maintcp=16261"}, MinDisk: "3Gi", MinRAM: "512Mi", MinCPU: "0.25", Category: "zomboid", Smart: true, PackMeta: true}, {Target: "csgoserver", Artifact: "artifacts.druid.gg/druid-team/scroll-lgsm:csgoserver-prebuild", Source: "./scrolls/lgsm/csgoserver", Image: steamImage, Ports: []string{"query=27005/udp", "main=27015/udp", "sourcetv=27020/udp", "steam=27015"}, BuildDisk: "45Gi", Category: "csgo", Smart: true, PackMeta: true}, - {Target: "rust-vanilla", Artifact: "artifacts.druid.gg/druid-team/scroll-rust-vanilla:latest-prebuild", Source: "./scrolls/rust/rust-vanilla/latest", Image: steamImage, Ports: []string{"main=28015/udp", "query=28017/udp", "rcon=28016", "rustplus=28082"}, MinDisk: "10Gi", BuildDisk: "25Gi", MinRAM: "6Gi", MinCPU: "1", Category: "rust", Smart: true}, - {Target: "rust-oxide", Artifact: "artifacts.druid.gg/druid-team/scroll-rust-oxide:latest-prebuild", Source: "./scrolls/rust/rust-oxide/latest", Image: steamImage, Ports: []string{"main=28015/udp", "query=28017/udp", "rcon=28016", "rustplus=28082"}, MinDisk: "10Gi", BuildDisk: "25Gi", MinRAM: "6Gi", MinCPU: "1", Category: "rust", Smart: true}, + {Target: "rust-vanilla", Artifact: "artifacts.druid.gg/druid-team/scroll-rust-vanilla:latest-prebuild", Source: "./scrolls/rust/rust-vanilla/latest", Image: steamImage, Ports: []string{"main=/udp", "query=/udp", "rcon", "rustplus"}, MinDisk: "10Gi", BuildDisk: "25Gi", MinRAM: "6Gi", MinCPU: "1", Category: "rust", Smart: true}, + {Target: "rust-oxide", Artifact: "artifacts.druid.gg/druid-team/scroll-rust-oxide:latest-prebuild", Source: "./scrolls/rust/rust-oxide/latest", Image: steamImage, Ports: []string{"main=/udp", "query=/udp", "rcon", "rustplus"}, MinDisk: "10Gi", BuildDisk: "25Gi", MinRAM: "6Gi", MinCPU: "1", Category: "rust", Smart: true}, } sort.Slice(specs, func(i, j int) bool { return specs[i].Target < specs[j].Target }) return specs diff --git a/scripts/prebuild/main_test.go b/scripts/prebuild/main_test.go index 4392aa8e..90638b75 100644 --- a/scripts/prebuild/main_test.go +++ b/scripts/prebuild/main_test.go @@ -89,20 +89,50 @@ func TestDayZPrebuildRequiresSteamCredentials(t *testing.T) { } } -func TestRustPrebuildPortsAreConcrete(t *testing.T) { +func TestRustPrebuildPortsAreDynamic(t *testing.T) { specs, err := selectSpecs("rust-vanilla,rust-oxide") if err != nil { t.Fatal(err) } for _, spec := range specs { - for _, port := range spec.Ports { - if port == "main=/udp" || port == "query=/udp" || port == "rcon" || port == "rustplus" { - t.Fatalf("%s has non-concrete port %q", spec.Target, port) + want := []string{"main=/udp", "query=/udp", "rcon", "rustplus"} + if len(spec.Ports) != len(want) { + t.Fatalf("%s ports = %#v", spec.Target, spec.Ports) + } + for index := range want { + if spec.Ports[index] != want[index] { + t.Fatalf("%s ports = %#v, want %#v", spec.Target, spec.Ports, want) } } } } +func TestSharedPortPrebuildsRemainConcrete(t *testing.T) { + tests := []struct { + target string + want []string + }{ + {target: "cs2server", want: []string{"main=27015/udp", "rcon=27015"}}, + {target: "pzserver", want: []string{"main=16261/udp", "main2=16262/udp", "maintcp=16261"}}, + } + for _, test := range tests { + t.Run(test.target, func(t *testing.T) { + specs, err := selectSpecs(test.target) + if err != nil { + t.Fatal(err) + } + if len(specs) != 1 || len(specs[0].Ports) != len(test.want) { + t.Fatalf("specs = %#v", specs) + } + for index := range test.want { + if specs[0].Ports[index] != test.want[index] { + t.Fatalf("ports = %#v, want %#v", specs[0].Ports, test.want) + } + } + }) + } +} + func TestValidateRequiredEnvFailsBeforePrebuild(t *testing.T) { t.Setenv("PREBUILD_TEST_REQUIRED", "") err := validateRequiredEnv(prebuildSpec{Target: "test", RequiredEnv: []string{"PREBUILD_TEST_REQUIRED"}}) diff --git a/scripts/push.sh b/scripts/push.sh index 2970c528..63e4aa55 100755 --- a/scripts/push.sh +++ b/scripts/push.sh @@ -174,16 +174,16 @@ push_release_artifacts() { run druid push artifacts.druid.gg/druid-team/scroll-minecraft-forge:1.21.6 ./scrolls/minecraft/forge/1.21.6 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.249 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft run druid push artifacts.druid.gg/druid-team/scroll-minecraft-forge:1.21.7 ./scrolls/minecraft/forge/1.21.7 -p main=25565 -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.249 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft run druid push artifacts.druid.gg/druid-team/scroll-minecraft-cuberite:latest ./scrolls/minecraft/cuberite/latest -p main=25565 -p webpanel=8080 -i artifacts.druid.gg/druid-team/druid:v0.1.249 --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category minecraft - run druid push artifacts.druid.gg/druid-team/scroll-rust-oxide:latest ./scrolls/rust/rust-oxide/latest -p main=28015/udp -p query=28017/udp -p rcon=28016 -p rustplus=28082 -i artifacts.druid.gg/druid-team/druid:v0.1.249-steamcmd --min-disk 10Gi --min-ram 6Gi --min-cpu 1 --smart --category rust - run druid push artifacts.druid.gg/druid-team/scroll-rust-vanilla:latest ./scrolls/rust/rust-vanilla/latest -p main=28015/udp -p query=28017/udp -p rcon=28016 -p rustplus=28082 -i artifacts.druid.gg/druid-team/druid:v0.1.249-steamcmd --min-disk 10Gi --min-ram 6Gi --min-cpu 1 --smart --category rust + run druid push artifacts.druid.gg/druid-team/scroll-rust-oxide:latest ./scrolls/rust/rust-oxide/latest -p main=/udp -p query=/udp -p rcon -p rustplus -i artifacts.druid.gg/druid-team/druid:v0.1.249-steamcmd --min-disk 10Gi --min-ram 6Gi --min-cpu 1 --smart --category rust + run druid push artifacts.druid.gg/druid-team/scroll-rust-vanilla:latest ./scrolls/rust/rust-vanilla/latest -p main=/udp -p query=/udp -p rcon -p rustplus -i artifacts.druid.gg/druid-team/druid:v0.1.249-steamcmd --min-disk 10Gi --min-ram 6Gi --min-cpu 1 --smart --category rust run druid push artifacts.druid.gg/druid-team/scroll-lgsm:pwserver ./scrolls/lgsm/pwserver -p main=8211/udp -p rcon=25575 -i artifacts.druid.gg/druid-team/druid:v0.1.249-steamcmd -m --min-disk 7Gi --min-ram 2Gi --min-cpu 0.5 --smart --category palworld - run druid push artifacts.druid.gg/druid-team/scroll-lgsm:arkserver ./scrolls/lgsm/arkserver -p main=7777/udp -p query=27015/udp -p rcon=27020 -i artifacts.druid.gg/druid-team/druid:v0.1.249-steamcmd -m --min-disk 25Gi --min-ram 7Gi --min-cpu 0.5 --smart --category ark - run druid push artifacts.druid.gg/druid-team/scroll-lgsm:untserver ./scrolls/lgsm/untserver -p main=27015/udp -p mainv6=27016 -i artifacts.druid.gg/druid-team/druid:v0.1.249-steamcmd -m --min-disk 7Gi --min-ram 1Gi --min-cpu 0.5 --smart --category unturned - run druid push artifacts.druid.gg/druid-team/scroll-lgsm:dayzserver ./scrolls/lgsm/dayzserver -p main=2302/udp -p battle-eye=2304/udp -p query=27016/udp -i artifacts.druid.gg/druid-team/druid:v0.1.249-steamcmd -m --min-disk 7Gi --min-ram 5Gi --min-cpu 1 --category dayz + run druid push artifacts.druid.gg/druid-team/scroll-lgsm:arkserver ./scrolls/lgsm/arkserver -p main=/udp -p query=/udp -p rcon -i artifacts.druid.gg/druid-team/druid:v0.1.249-steamcmd -m --min-disk 25Gi --min-ram 7Gi --min-cpu 0.5 --smart --category ark + run druid push artifacts.druid.gg/druid-team/scroll-lgsm:untserver ./scrolls/lgsm/untserver -p main=/udp -p mainv6=27016 -i artifacts.druid.gg/druid-team/druid:v0.1.249-steamcmd -m --min-disk 7Gi --min-ram 1Gi --min-cpu 0.5 --smart --category unturned + run druid push artifacts.druid.gg/druid-team/scroll-lgsm:dayzserver ./scrolls/lgsm/dayzserver -p main=/udp -p battle-eye=2304/udp -p query=27016/udp -i artifacts.druid.gg/druid-team/druid:v0.1.249-steamcmd -m --min-disk 7Gi --min-ram 5Gi --min-cpu 1 --category dayz run druid push artifacts.druid.gg/druid-team/scroll-lgsm:sdtdserver ./scrolls/lgsm/sdtdserver -p main=26900/udp -p main2=26902/udp -p maintcp=26900 -i artifacts.druid.gg/druid-team/druid:v0.1.249-steamcmd -m --min-disk 20Gi --min-ram 2Gi --min-cpu 0.5 --category 7days - run druid push artifacts.druid.gg/druid-team/scroll-lgsm:gmodserver ./scrolls/lgsm/gmodserver -p query=27005/udp -p main=27015/udp -p sourcetv=27020/udp -p steam=27015 -i artifacts.druid.gg/druid-team/druid:v0.1.249-steamcmd -m --min-disk 8Gi --min-ram 512Mi --min-cpu 0.25 --smart --category gmod - run druid push artifacts.druid.gg/druid-team/scroll-lgsm:cs2server ./scrolls/lgsm/cs2server -p main=27015/udp -p rcon=27015 -i artifacts.druid.gg/druid-team/druid:v0.1.249-steamcmd -m --min-disk 38Gi --min-ram 1Gi --min-cpu 0.5 --smart --category cs2 - run druid push artifacts.druid.gg/druid-team/scroll-lgsm:pzserver ./scrolls/lgsm/pzserver -p main=16261/udp -p main2=16262/udp -p maintcp=16261 -i artifacts.druid.gg/druid-team/druid:v0.1.249-steamcmd -m --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category zomboid + run druid push artifacts.druid.gg/druid-team/scroll-lgsm:gmodserver ./scrolls/lgsm/gmodserver -p query=27005/udp -p main=/udp -p sourcetv=27020/udp -p steam=27015 -i artifacts.druid.gg/druid-team/druid:v0.1.249-steamcmd -m --min-disk 8Gi --min-ram 512Mi --min-cpu 0.25 --smart --category gmod + run druid push artifacts.druid.gg/druid-team/scroll-lgsm:cs2server ./scrolls/lgsm/cs2server -p main=/udp -p rcon=27015 -i artifacts.druid.gg/druid-team/druid:v0.1.249-steamcmd -m --min-disk 38Gi --min-ram 1Gi --min-cpu 0.5 --smart --category cs2 + run druid push artifacts.druid.gg/druid-team/scroll-lgsm:pzserver ./scrolls/lgsm/pzserver -p main=/udp -p main2=/udp -i artifacts.druid.gg/druid-team/druid:v0.1.249-steamcmd -m --min-disk 3Gi --min-ram 512Mi --min-cpu 0.25 --smart --category zomboid run druid push artifacts.druid.gg/druid-team/scroll-lgsm:csgoserver ./scrolls/lgsm/csgoserver -p query=27005/udp -p main=27015/udp -p sourcetv=27020/udp -p steam=27015 -i artifacts.druid.gg/druid-team/druid:v0.1.249-steamcmd -m --smart --category csgo run druid push artifacts.druid.gg/druid-team/scroll-hytale:standalone ./scrolls/hytale/hytale-standalone -p main=5520/udp -i artifacts.druid.gg/druid-team/druid:v0.1.249 --min-disk 10Gi --min-ram 4Gi --min-cpu 1 -m --smart --category hytale run druid push artifacts.druid.gg/druid-team/scroll-hytale:latest ./scrolls/hytale/hytale-druid-gg -p main=5520/udp -i artifacts.druid.gg/druid-team/druid:v0.1.249 --min-disk 10Gi --min-ram 4Gi --min-cpu 1 -m --smart --category hytale diff --git a/scripts/validate-release-workflow/main.go b/scripts/validate-release-workflow/main.go index 40d72e65..d0796d05 100644 --- a/scripts/validate-release-workflow/main.go +++ b/scripts/validate-release-workflow/main.go @@ -18,11 +18,9 @@ var ( "-nix-steamcmd", } portOverrideNamePattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_-]*$`) - portOverridePattern = regexp.MustCompile(`^([A-Za-z][A-Za-z0-9_-]*)=([0-9]+)(?:/(tcp|udp|http|https))?$`) pzserverRequiredPorts = map[string]string{ - "main": "16261/udp", - "main2": "16262/udp", - "maintcp": "16261", + "main": "/udp", + "main2": "/udp", } ) @@ -103,23 +101,51 @@ func validatePortOverrides(fields []string) (map[string]string, error) { continue } override := fields[i+1] - matches := portOverridePattern.FindStringSubmatch(override) - if matches == nil { - failures = append(failures, fmt.Sprintf("invalid %s override %q; expected name=port or name=port/protocol", field, override)) + name, value, hasValue := strings.Cut(override, "=") + if !portOverrideNamePattern.MatchString(name) { + failures = append(failures, fmt.Sprintf("invalid %s override %q; invalid port name", field, override)) + continue + } + if !hasValue { + value = "" + } + portText := value + protocol := "" + if strings.Contains(value, "/") { + parts := strings.Split(value, "/") + if len(parts) != 2 || parts[1] == "" { + failures = append(failures, fmt.Sprintf("invalid %s override %q; expected name=port or name=port/protocol", field, override)) + continue + } + portText = parts[0] + protocol = parts[1] + switch protocol { + case "tcp", "udp", "http", "https": + default: + failures = append(failures, fmt.Sprintf("invalid %s override %q; unsupported protocol", field, override)) + continue + } + } + if portText == "" || portText == "0" { + if protocol == "http" || protocol == "https" { + failures = append(failures, fmt.Sprintf("invalid %s override %q; dynamic HTTP ports are not supported", field, override)) + continue + } + ports[name] = "0" + if protocol != "" { + ports[name] = "/" + protocol + } continue } - name := matches[1] - portText := matches[2] port, err := strconv.Atoi(portText) if err != nil || port < 1 || port > 65535 { - failures = append(failures, fmt.Sprintf("invalid %s override %q; port must be 1-65535", field, override)) + failures = append(failures, fmt.Sprintf("invalid %s override %q; port must be omitted or 1-65535", field, override)) continue } - if !portOverrideNamePattern.MatchString(name) { - failures = append(failures, fmt.Sprintf("invalid %s override %q; invalid port name", field, override)) - continue + ports[name] = portText + if protocol != "" { + ports[name] += "/" + protocol } - ports[name] = strings.TrimPrefix(override, name+"=") } if len(failures) > 0 { diff --git a/scripts/validate-release-workflow/main_test.go b/scripts/validate-release-workflow/main_test.go index 0abaec94..b183a246 100644 --- a/scripts/validate-release-workflow/main_test.go +++ b/scripts/validate-release-workflow/main_test.go @@ -17,14 +17,30 @@ func TestValidatePortOverridesAcceptsConcretePorts(t *testing.T) { } } -func TestValidatePortOverridesRejectsBlankAndBarePorts(t *testing.T) { +func TestValidatePortOverridesAcceptsDynamicPorts(t *testing.T) { for _, value := range []string{ "main", "main=", "main=/udp", "main=0", "main=0/udp", + } { + fields := []string{"druid", "push", "repo:tag", "./scroll", "-p", value} + if _, err := validatePortOverrides(fields); err != nil { + t.Fatalf("%s failed validation: %v", value, err) + } + } +} + +func TestValidatePortOverridesRejectsInvalidPorts(t *testing.T) { + for _, value := range []string{ "main=65536", + "main=/", + "main=/sctp", + "web=/http", + "web=0/https", + "main=123/udp/extra", + "1main=123", } { fields := []string{"druid", "push", "repo:tag", "./scroll", "-p", value} if _, err := validatePortOverrides(fields); err == nil { diff --git a/scripts/validate-scrolls.go b/scripts/validate-scrolls.go index cf8e35ca..f880a1d9 100644 --- a/scripts/validate-scrolls.go +++ b/scripts/validate-scrolls.go @@ -127,7 +127,10 @@ func validatePorts(raw any) (map[string]bool, error) { } rawPortNumber, hasPort := port["port"] if !hasPort { - return nil, fmt.Errorf("port %q missing port", name) + if protocol == "http" || protocol == "https" { + return nil, fmt.Errorf("port %q uses %s and requires a fixed port", name, protocol) + } + continue } portNumber, ok := asInt(rawPortNumber) if !ok { diff --git a/scripts/validate-scrolls_test.go b/scripts/validate-scrolls_test.go new file mode 100644 index 00000000..6d2e09da --- /dev/null +++ b/scripts/validate-scrolls_test.go @@ -0,0 +1,27 @@ +package main + +import ( + "strings" + "testing" +) + +func TestValidatePortsAllowsDynamicTransportPorts(t *testing.T) { + ports := []any{ + map[string]any{"name": "tcp", "protocol": "tcp"}, + map[string]any{"name": "udp", "protocol": "udp"}, + } + if _, err := validatePorts(ports); err != nil { + t.Fatal(err) + } +} + +func TestValidatePortsRejectsDynamicHTTPPorts(t *testing.T) { + for _, protocol := range []string{"http", "https"} { + _, err := validatePorts([]any{ + map[string]any{"name": "web", "protocol": protocol}, + }) + if err == nil || !strings.Contains(err.Error(), "requires a fixed port") { + t.Fatalf("%s error = %v", protocol, err) + } + } +} diff --git a/scrolls/lgsm/.build/scroll.yaml.tmpl b/scrolls/lgsm/.build/scroll.yaml.tmpl index adbe5eeb..f8cf16f9 100644 --- a/scrolls/lgsm/.build/scroll.yaml.tmpl +++ b/scrolls/lgsm/.build/scroll.yaml.tmpl @@ -11,7 +11,9 @@ ports: {{- range .Ports }} - name: {{ .Name }} protocol: {{ .Protocol }} +{{- if .Port }} port: {{ .Port }} +{{- end }} {{- if .Description }} description: {{ .Description }} {{- end }} @@ -117,6 +119,9 @@ commands: {{- range split ";" .Vars.extra_expected_ports }} - name: {{ . }} {{- end }} +{{- end }} +{{- if .Vars.rcon_port }} + - name: rcon {{- end }} mounts: - path: "/server" diff --git a/scrolls/lgsm/.build/vars.json b/scrolls/lgsm/.build/vars.json index 6fcb09bf..15167601 100644 --- a/scrolls/lgsm/.build/vars.json +++ b/scrolls/lgsm/.build/vars.json @@ -1,12 +1,13 @@ { "arkserver": { "rcon": "enabled", - "port": "main=7777/udp;query=27015/udp;rcon=27020", + "port": "main=/udp;query=/udp;rcon", "lua_query_game_name": "ARK: Survival Evolved", "lua_query_folder": "ark_survival_evolved", "lua_query_map": "server idle", "lua_query_servername": "Druid.gg Server (idle) - join to start", "main_port_protocol": "udp", + "keep_alive_traffic": "6mb/30m", "start_delay": "0", "rcon_port": "true", "lua_steam_app_id": "346110", @@ -23,17 +24,18 @@ "lua_query_port": "main", "lua_query_start_on_unknown_packet": "yes", "lua_steam_app_id": "730", + "keep_alive_traffic": "6mb/30m", "dependencies": "bc;binutils;bzip2;cpio;file;jq;pkgsi686Linux.gcc;netcat;pigz;python3;tmux;unzip;util-linux;moreutils;iproute2" }, "pwserver": { "port": "main=8211/udp;rcon=25575", "main_port_protocol": "udp", - "keep_alive_traffic": "1mb/30m", + "keep_alive_traffic": "6mb/30m", "lua_steam_app_id": "1623730", "dependencies": "bc;binutils;bzip2;cpio;file;jq;pkgsi686Linux.gcc;netcat;pigz;python3;tmux;unzip;util-linux;moreutils;iproute2" }, "gmodserver": { - "port": "query=27005/udp;main=27015/udp;sourcetv=27020/udp;steam=27015", + "port": "query=27005/udp;main=/udp;sourcetv=27020/udp;steam=27015", "lua_query_game_name": "Idle Druid Server", "lua_query_folder": "garrysmod", "lua_query_keywords": " ver:250717", @@ -44,6 +46,7 @@ "lua_query_port": "main", "lua_query_start_on_unknown_packet": "yes", "lua_steam_app_id": "4000", + "keep_alive_traffic": "6mb/30m", "dependencies": "bc;binutils;bzip2;cpio;file;jq;pkgsi686Linux.gcc;netcat;pigz;python3;tmux;unzip;util-linux;moreutils;iproute2" }, "sdtdserver": { @@ -56,17 +59,18 @@ "lua_query_start_on_unknown_packet": "yes", "main_port_protocol": "udp", "lua_steam_app_id": "251570", + "keep_alive_traffic": "6mb/30m", "dependencies": "bc;binutils;bzip2;cpio;file;jq;pkgsi686Linux.gcc;netcat;pigz;python3;tmux;unzip;util-linux;moreutils;iproute2" }, "dayzserver": { - "port": "main=2302/udp;battle-eye=2304/udp;query=27016/udp", + "port": "main=/udp;battle-eye=2304/udp;query=27016/udp", "main_port_protocol": "udp", - "keep_alive_traffic": "10kb/30m", + "keep_alive_traffic": "60kb/30m", "lua_steam_app_id": "221100", "dependencies": "bc;binutils;bzip2;cpio;file;jq;pkgsi686Linux.gcc;netcat;pigz;python3;tmux;unzip;util-linux;moreutils;iproute2" }, "untserver": { - "port": "main=27015/udp;mainv6=27016", + "port": "main=/udp;mainv6=27016", "lua_query_game_name": "Unturned Server", "lua_query_folder": "Unturned", "lua_query_map": "server idle", @@ -74,6 +78,7 @@ "lua_query_port": "main", "lua_query_start_on_unknown_packet": "yes", "lua_steam_app_id": "304930", + "keep_alive_traffic": "6mb/30m", "dependencies": "bc;binutils;bzip2;cpio;file;jq;pkgsi686Linux.gcc;netcat;pigz;python3;tmux;unzip;util-linux;moreutils;iproute2" }, "csgoserver": { @@ -86,6 +91,7 @@ "lua_query_start_on_unknown_packet": "yes", "ppm": "600", "lua_steam_app_id": "730", + "keep_alive_traffic": "6mb/30m", "dependencies": "bc;binutils;bzip2;cpio;file;jq;pkgsi686Linux.gcc;netcat;pigz;python3;tmux;unzip;util-linux;moreutils;iproute2" }, "terrariaserver": { @@ -100,7 +106,7 @@ }, "pzserver": { "port": "main=16261/udp;main2=16262/udp;maintcp=16261", - "keep_alive_traffic": "1mb/30m", + "keep_alive_traffic": "6mb/30m", "lua_query_game_name": "Project Zomboid", "lua_query_folder": "zomboid", "lua_query_map": "server idle", diff --git a/scrolls/lgsm/arkserver/scroll.yaml b/scrolls/lgsm/arkserver/scroll.yaml index 4814e450..c1b71e8a 100644 --- a/scrolls/lgsm/arkserver/scroll.yaml +++ b/scrolls/lgsm/arkserver/scroll.yaml @@ -47,15 +47,12 @@ chunks: ports: - name: main protocol: udp - port: 7777 description: Main game port. Use this port inside of your game client to connect to the server. Depending on the game you might need the query port to connect. - name: query protocol: udp - port: 27015 description: Steam Query Port. Use this to connect via the Steam client. - name: rcon protocol: tcp - port: 27020 commands: console: needs: @@ -94,6 +91,7 @@ commands: keepAliveTraffic: 6mb/30m - name: main keepAliveTraffic: 6mb/30m + - name: rcon mounts: - path: "/server" working_dir: "/server" diff --git a/scrolls/lgsm/dayzserver/scroll.yaml b/scrolls/lgsm/dayzserver/scroll.yaml index e58a0925..89a9830e 100644 --- a/scrolls/lgsm/dayzserver/scroll.yaml +++ b/scrolls/lgsm/dayzserver/scroll.yaml @@ -6,7 +6,6 @@ keepAlivePPM: 5 ports: - name: main protocol: udp - port: 2302 description: Main game port. Use this port inside of your game client to connect to the server. Depending on the game you might need the query port to connect. - name: battle-eye protocol: udp diff --git a/scrolls/lgsm/gmodserver/scroll.yaml b/scrolls/lgsm/gmodserver/scroll.yaml index e59b45bb..b7302100 100644 --- a/scrolls/lgsm/gmodserver/scroll.yaml +++ b/scrolls/lgsm/gmodserver/scroll.yaml @@ -10,7 +10,6 @@ ports: description: Steam Query Port. Use this to connect via the Steam client. - name: main protocol: udp - port: 27015 description: Main game port. Use this port inside of your game client to connect to the server. Depending on the game you might need the query port to connect. - name: sourcetv protocol: udp diff --git a/scrolls/lgsm/untserver/scroll.yaml b/scrolls/lgsm/untserver/scroll.yaml index 8eb4dcc6..a60439e1 100644 --- a/scrolls/lgsm/untserver/scroll.yaml +++ b/scrolls/lgsm/untserver/scroll.yaml @@ -6,7 +6,6 @@ keepAlivePPM: 5 ports: - name: main protocol: udp - port: 27015 description: Main game port. Use this port inside of your game client to connect to the server. Depending on the game you might need the query port to connect. - name: mainv6 protocol: tcp diff --git a/scrolls/rust/rust-oxide/latest/scroll.yaml b/scrolls/rust/rust-oxide/latest/scroll.yaml index 87f0aead..207e5e7f 100644 --- a/scrolls/rust/rust-oxide/latest/scroll.yaml +++ b/scrolls/rust/rust-oxide/latest/scroll.yaml @@ -5,17 +5,13 @@ app_version: latest ports: - name: main protocol: udp - port: 28015 - name: query protocol: udp description: Steam Query Port. Use this to connect via the Steam client. - port: 28017 - name: rustplus protocol: tcp - port: 28082 - name: rcon protocol: tcp - port: 28016 commands: start: needs: @@ -31,6 +27,7 @@ commands: keepAliveTraffic: 3mb/30m - name: rustplus keepAliveTraffic: 3mb/30m + - name: rcon mounts: - path: "/runtime" sub_path: "." @@ -40,6 +37,7 @@ commands: DRUID_PORT_MAIN_COLDSTARTER: "generic" DRUID_PORT_QUERY_COLDSTARTER: "packet_handler/query.lua" DRUID_PORT_RUSTPLUS_COLDSTARTER: "generic" + DRUID_PORT_RCON_COLDSTARTER: "generic" DRUID_COLDSTARTER_VAR_GAME_NAME: "Rust" DRUID_COLDSTARTER_VAR_GAME_STEAM_FOLDER: "rust" DRUID_COLDSTARTER_VAR_GAME_STEAM_ID: "0" @@ -64,6 +62,7 @@ commands: keepAliveTraffic: 3mb/30m - name: rustplus keepAliveTraffic: 3mb/30m + - name: rcon stop: procedures: - type: signal diff --git a/scrolls/rust/rust-vanilla/latest/scroll.yaml b/scrolls/rust/rust-vanilla/latest/scroll.yaml index 49edc2d9..06507696 100644 --- a/scrolls/rust/rust-vanilla/latest/scroll.yaml +++ b/scrolls/rust/rust-vanilla/latest/scroll.yaml @@ -5,17 +5,13 @@ app_version: latest ports: - name: main protocol: udp - port: 28015 - name: query protocol: udp description: Steam Query Port. Use this to connect via the Steam client. - port: 28017 - name: rustplus protocol: tcp - port: 28082 - name: rcon protocol: tcp - port: 28016 commands: start: needs: @@ -31,6 +27,7 @@ commands: keepAliveTraffic: 3mb/30m - name: rustplus keepAliveTraffic: 3mb/30m + - name: rcon mounts: - path: "/runtime" sub_path: "." @@ -40,6 +37,7 @@ commands: DRUID_PORT_MAIN_COLDSTARTER: "generic" DRUID_PORT_QUERY_COLDSTARTER: "packet_handler/query.lua" DRUID_PORT_RUSTPLUS_COLDSTARTER: "generic" + DRUID_PORT_RCON_COLDSTARTER: "generic" DRUID_COLDSTARTER_VAR_GAME_NAME: "Rust" DRUID_COLDSTARTER_VAR_GAME_STEAM_FOLDER: "rust" DRUID_COLDSTARTER_VAR_GAME_STEAM_ID: "0" @@ -64,6 +62,7 @@ commands: keepAliveTraffic: 3mb/30m - name: rustplus keepAliveTraffic: 3mb/30m + - name: rcon stop: procedures: - type: signal