Skip to content

Commit 30128b9

Browse files
committed
feat: add namespace support for runtime resources
- Introduced a new `namespace` field in the OpenAPI specification for runtime resources, applicable to Kubernetes backends. - Updated the runtime supervisor and related services to handle the new namespace parameter during scroll creation and management. - Enhanced error handling and logging in the runtime daemon for better observability. - Refactored related tests to accommodate the namespace changes and ensure proper functionality.
1 parent aa519e7 commit 30128b9

44 files changed

Lines changed: 1725 additions & 518 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

api/openapi.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ components:
8989
owner_id:
9090
type: string
9191
description: Runtime owner id used for customer-facing route authorization.
92+
namespace:
93+
type: string
94+
description: Kubernetes namespace for runtime resources. Ignored by non-Kubernetes backends.
9295
registry_credentials:
9396
type: array
9497
items:
@@ -110,6 +113,9 @@ components:
110113
owner_id:
111114
type: string
112115
description: Runtime owner id used for customer-facing route authorization.
116+
namespace:
117+
type: string
118+
description: Kubernetes namespace for runtime resources. Ignored by non-Kubernetes backends.
113119
registry_credentials:
114120
type: array
115121
items:

apps/druid-coldstarter/adapters/cli/root.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,12 @@ import (
77
"os/signal"
88
"syscall"
99

10-
"github.com/highcard-dev/daemon/apps/druid-coldstarter/adapters/filesystem"
1110
"github.com/highcard-dev/daemon/apps/druid-coldstarter/core/services"
1211
"github.com/spf13/cobra"
1312
)
1413

1514
const (
16-
rootEnv = "DRUID_ROOT"
17-
statusFileEnv = "DRUID_COLDSTARTER_STATUS_FILE"
15+
rootEnv = "DRUID_ROOT"
1816
)
1917

2018
func NewRootCommand() *cobra.Command {
@@ -28,7 +26,7 @@ func NewRootCommand() *cobra.Command {
2826
}
2927
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, syscall.SIGINT)
3028
defer stop()
31-
return services.NewColdstarterService(filesystem.NewStatusWriter()).Run(ctx, root, os.Getenv(statusFileEnv))
29+
return services.NewColdstarterService().Run(ctx, root)
3230
},
3331
}
3432
cmd.SilenceUsage = true

apps/druid-coldstarter/adapters/cli/root_test.go

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,10 @@ import (
77

88
func TestRootCommandHasNoRuntimeFlags(t *testing.T) {
99
cmd := NewRootCommand()
10-
if cmd.Flags().Lookup("root") != nil {
11-
t.Fatal("did not expect root flag")
12-
}
13-
if cmd.Flags().Lookup("status-file") != nil {
14-
t.Fatal("did not expect status-file flag")
15-
}
16-
if cmd.Flags().Lookup("scroll-root") != nil {
17-
t.Fatal("did not expect scroll-root flag")
18-
}
19-
if cmd.Flags().Lookup("runtime-config") != nil {
20-
t.Fatal("did not expect runtime-config flag")
10+
for _, flag := range []string{"root", "scroll-root", "runtime-config", "status" + "-" + "file"} {
11+
if cmd.Flags().Lookup(flag) != nil {
12+
t.Fatalf("did not expect %s flag", flag)
13+
}
2114
}
2215
}
2316

apps/druid-coldstarter/adapters/filesystem/status_writer.go

Lines changed: 0 additions & 46 deletions
This file was deleted.

apps/druid-coldstarter/core/ports/status_writer.go

Lines changed: 0 additions & 7 deletions
This file was deleted.

apps/druid-coldstarter/core/services/coldstarter.go

Lines changed: 83 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,50 +3,111 @@ package services
33
import (
44
"context"
55
"fmt"
6+
"os"
7+
"path/filepath"
8+
"strconv"
9+
"strings"
10+
"time"
611

7-
"github.com/highcard-dev/daemon/apps/druid-coldstarter/core/ports"
12+
"github.com/highcard-dev/daemon/internal/core/domain"
813
"github.com/highcard-dev/daemon/internal/core/services"
914
"github.com/highcard-dev/daemon/internal/utils/logger"
1015
"go.uber.org/zap"
1116
)
1217

13-
type ColdstarterService struct {
14-
statusWriter ports.StatusWriter
18+
type ColdstarterService struct{}
19+
20+
type envPortService struct {
21+
ports []*domain.AugmentedPort
1522
}
1623

17-
func NewColdstarterService(statusWriter ports.StatusWriter) *ColdstarterService {
18-
return &ColdstarterService{statusWriter: statusWriter}
24+
func NewColdstarterService() *ColdstarterService {
25+
return &ColdstarterService{}
1926
}
2027

21-
func (s *ColdstarterService) Run(ctx context.Context, root string, statusFile string) error {
22-
scrollService, err := services.NewScrollService(root)
28+
func (s *ColdstarterService) Run(ctx context.Context, root string) error {
29+
portService, err := portServiceFromEnv(root)
2330
if err != nil {
24-
return fmt.Errorf("failed to load scroll: %w", err)
25-
}
26-
27-
currentScroll := scrollService.GetCurrent()
28-
if len(currentScroll.Ports) == 0 {
29-
return fmt.Errorf("no ports found in scroll")
31+
return err
3032
}
3133

32-
logger.Log().Info("Coldstart scroll loaded", zap.String("name", currentScroll.Name), zap.Any("version", currentScroll.Version), zap.Any("ports", currentScroll.Ports))
34+
logger.Log().Info("Coldstart ports loaded", zap.Any("ports", portService.GetPorts()))
3335

34-
portService := services.NewPortServiceWithScrollFile(&currentScroll.File)
35-
coldStarter := services.NewColdStarter(portService, nil, scrollService.GetDir())
36+
coldStarter := services.NewColdStarter(portService, nil, root)
3637

3738
finish := coldStarter.Start(ctx)
3839
select {
3940
case <-ctx.Done():
4041
coldStarter.Stop()
4142
return ctx.Err()
42-
case port := <-finish:
43+
case <-finish:
4344
coldStarter.Stop()
44-
if statusFile != "" && s.statusWriter != nil {
45-
if err := s.statusWriter.Write(root, statusFile, port); err != nil {
46-
return err
47-
}
48-
}
4945
logger.Log().Info("Coldstarter finished")
5046
return nil
5147
}
5248
}
49+
50+
func (s *envPortService) GetPorts() []*domain.AugmentedPort {
51+
return s.ports
52+
}
53+
54+
func portServiceFromEnv(root string) (*envPortService, error) {
55+
ports := []*domain.AugmentedPort{}
56+
vars := map[string]string{}
57+
for _, entry := range os.Environ() {
58+
key, value, ok := strings.Cut(entry, "=")
59+
if !ok {
60+
continue
61+
}
62+
if name, ok := strings.CutPrefix(key, "DRUID_COLDSTARTER_VAR_"); ok {
63+
if name != strings.ToUpper(name) {
64+
return nil, fmt.Errorf("%s must be uppercase", key)
65+
}
66+
vars[name] = value
67+
continue
68+
}
69+
if !strings.HasPrefix(key, "DRUID_PORT_") || !strings.HasSuffix(key, "_COLDSTARTER") {
70+
continue
71+
}
72+
if key != strings.ToUpper(key) {
73+
return nil, fmt.Errorf("%s must be uppercase", key)
74+
}
75+
handler := value
76+
suffix := strings.TrimSuffix(strings.TrimPrefix(key, "DRUID_PORT_"), "_COLDSTARTER")
77+
portValue := os.Getenv("DRUID_PORT_" + suffix)
78+
if portValue == "" {
79+
return nil, fmt.Errorf("DRUID_PORT_%s is required when %s is set", suffix, key)
80+
}
81+
if handler == "" {
82+
return nil, fmt.Errorf("%s must not be empty", key)
83+
}
84+
if handler != "generic" {
85+
path := filepath.Join(root, filepath.Clean(handler))
86+
if rel, err := filepath.Rel(root, path); err != nil || rel == ".." || filepath.IsAbs(rel) || strings.HasPrefix(rel, "../") {
87+
return nil, fmt.Errorf("%s must be generic or a path below DRUID_ROOT", key)
88+
}
89+
}
90+
port, err := strconv.Atoi(portValue)
91+
if err != nil {
92+
return nil, fmt.Errorf("DRUID_PORT_%s must be a port number: %w", suffix, err)
93+
}
94+
protocol := strings.ToLower(os.Getenv("DRUID_PORT_" + suffix + "_PROTOCOL"))
95+
if protocol == "" {
96+
protocol = "tcp"
97+
}
98+
ports = append(ports, &domain.AugmentedPort{
99+
Port: domain.Port{
100+
Name: strings.ToLower(suffix),
101+
Port: port,
102+
Protocol: protocol,
103+
},
104+
ColdstarterHandler: handler,
105+
ColdstarterVars: vars,
106+
InactiveSince: time.Now(),
107+
})
108+
}
109+
if len(ports) == 0 {
110+
return nil, fmt.Errorf("no coldstarter ports configured")
111+
}
112+
return &envPortService{ports: ports}, nil
113+
}

apps/druid-coldstarter/core/services/coldstarter_test.go

Lines changed: 96 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,33 +6,23 @@ import (
66
"os"
77
"path/filepath"
88
"strconv"
9+
"strings"
910
"testing"
1011
"time"
11-
12-
"github.com/highcard-dev/daemon/apps/druid-coldstarter/adapters/filesystem"
1312
)
1413

15-
func TestColdstarterRunServesGenericPortAndWritesStatus(t *testing.T) {
14+
func TestColdstarterRunServesGenericPortFromEnv(t *testing.T) {
1615
root := t.TempDir()
1716
port := freeTCPPort(t)
18-
scroll := []byte(`name: test/coldstarter
19-
version: 0.1.0
20-
ports:
21-
- name: main
22-
protocol: tcp
23-
port: ` + port + `
24-
sleep_handler: generic
25-
commands: {}
26-
`)
27-
if err := os.WriteFile(filepath.Join(root, "scroll.yaml"), scroll, 0644); err != nil {
28-
t.Fatal(err)
29-
}
17+
t.Setenv("DRUID_PORT_MAIN", port)
18+
t.Setenv("DRUID_PORT_MAIN_PROTOCOL", "tcp")
19+
t.Setenv("DRUID_PORT_MAIN_COLDSTARTER", "generic")
3020

3121
ctx, cancel := context.WithCancel(context.Background())
3222
defer cancel()
3323
errCh := make(chan error, 1)
3424
go func() {
35-
errCh <- NewColdstarterService(filesystem.NewStatusWriter()).Run(ctx, root, ".coldstarter.json")
25+
errCh <- NewColdstarterService().Run(ctx, root)
3626
}()
3727

3828
conn := dialTCP(t, "127.0.0.1:"+port)
@@ -47,8 +37,96 @@ commands: {}
4737
case <-time.After(3 * time.Second):
4838
t.Fatal("coldstarter did not finish")
4939
}
50-
if _, err := os.Stat(filepath.Join(root, ".coldstarter.json")); err != nil {
51-
t.Fatalf("status file missing: %v", err)
40+
if _, err := os.Stat(filepath.Join(root, ".coldstarter.json")); !os.IsNotExist(err) {
41+
t.Fatalf("status file exists or stat failed: %v", err)
42+
}
43+
}
44+
45+
func TestColdstarterRunExitsFromSecondaryGenericPort(t *testing.T) {
46+
root := t.TempDir()
47+
mainPort := freeTCPPort(t)
48+
rconPort := freeTCPPort(t)
49+
t.Setenv("DRUID_PORT_MAIN", mainPort)
50+
t.Setenv("DRUID_PORT_MAIN_PROTOCOL", "tcp")
51+
t.Setenv("DRUID_PORT_MAIN_COLDSTARTER", "generic")
52+
t.Setenv("DRUID_PORT_RCON", rconPort)
53+
t.Setenv("DRUID_PORT_RCON_PROTOCOL", "tcp")
54+
t.Setenv("DRUID_PORT_RCON_COLDSTARTER", "generic")
55+
56+
errCh := make(chan error, 1)
57+
go func() {
58+
errCh <- NewColdstarterService().Run(context.Background(), root)
59+
}()
60+
61+
conn := dialTCP(t, "127.0.0.1:"+rconPort)
62+
_, _ = conn.Write([]byte("wake"))
63+
_ = conn.Close()
64+
65+
select {
66+
case err := <-errCh:
67+
if err != nil {
68+
t.Fatal(err)
69+
}
70+
case <-time.After(3 * time.Second):
71+
t.Fatal("coldstarter did not finish")
72+
}
73+
}
74+
75+
func TestColdstarterRejectsMissingPortEnv(t *testing.T) {
76+
t.Setenv("DRUID_PORT_MAIN_COLDSTARTER", "generic")
77+
78+
err := NewColdstarterService().Run(context.Background(), t.TempDir())
79+
if err == nil || !strings.Contains(err.Error(), "DRUID_PORT_MAIN is required") {
80+
t.Fatalf("err = %v", err)
81+
}
82+
}
83+
84+
func TestColdstarterRejectsPathTraversalHandler(t *testing.T) {
85+
t.Setenv("DRUID_PORT_MAIN", freeTCPPort(t))
86+
t.Setenv("DRUID_PORT_MAIN_COLDSTARTER", "../minecraft.lua")
87+
88+
err := NewColdstarterService().Run(context.Background(), t.TempDir())
89+
if err == nil || !strings.Contains(err.Error(), "path below DRUID_ROOT") {
90+
t.Fatalf("err = %v", err)
91+
}
92+
}
93+
94+
func TestColdstarterRequiresConfiguredPorts(t *testing.T) {
95+
err := NewColdstarterService().Run(context.Background(), t.TempDir())
96+
if err == nil || !strings.Contains(err.Error(), "no coldstarter ports configured") {
97+
t.Fatalf("err = %v", err)
98+
}
99+
}
100+
101+
func TestColdstarterAcceptsRelativeLuaHandler(t *testing.T) {
102+
root := t.TempDir()
103+
if err := os.MkdirAll(filepath.Join(root, "packet_handler"), 0755); err != nil {
104+
t.Fatal(err)
105+
}
106+
t.Setenv("DRUID_PORT_MAIN", freeTCPPort(t))
107+
t.Setenv("DRUID_PORT_MAIN_COLDSTARTER", "packet_handler/minecraft.lua")
108+
t.Setenv("DRUID_COLDSTARTER_VAR_SERVER_LIST_NAME", "Druid idle")
109+
110+
service, err := portServiceFromEnv(root)
111+
if err != nil {
112+
t.Fatal(err)
113+
}
114+
if got := service.GetPorts()[0].ColdstarterHandler; got != "packet_handler/minecraft.lua" {
115+
t.Fatalf("handler = %q", got)
116+
}
117+
if got := service.GetPorts()[0].ColdstarterVars["SERVER_LIST_NAME"]; got != "Druid idle" {
118+
t.Fatalf("lua var = %q", got)
119+
}
120+
}
121+
122+
func TestColdstarterRejectsMixedCaseEnvNames(t *testing.T) {
123+
t.Setenv("DRUID_PORT_MAIN", freeTCPPort(t))
124+
t.Setenv("DRUID_PORT_MAIN_COLDSTARTER", "generic")
125+
t.Setenv("DRUID_COLDSTARTER_VAR_"+"ServerListName", "Druid idle")
126+
127+
err := NewColdstarterService().Run(context.Background(), t.TempDir())
128+
if err == nil || !strings.Contains(err.Error(), "must be uppercase") {
129+
t.Fatalf("err = %v", err)
52130
}
53131
}
54132

0 commit comments

Comments
 (0)