panel — open-source game server manager (public release)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
package dispatch
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
agentmodule "github.com/dbledeez/panel/agent/internal/module"
|
||||
modulepkg "github.com/dbledeez/panel/pkg/module"
|
||||
panelv1 "github.com/dbledeez/panel/proto/panel/v1"
|
||||
)
|
||||
|
||||
// handleRenderConfig re-renders the module's declared config_files into the
|
||||
// instance's host DataPath — the same modulepkg.Render call handleCreate
|
||||
// makes, minus the container recreate. For modules that bind-mount the
|
||||
// rendered output into the container (7dtd's serverconfig.xml.rendered),
|
||||
// os.WriteFile rewrites the host file in place (same inode), so the running
|
||||
// container sees the new content immediately and the entrypoint applies it
|
||||
// on the NEXT boot. The container itself is never restarted.
|
||||
//
|
||||
// Containers created BEFORE a config_file's bind-mount was added to the
|
||||
// module manifest don't have the bind at all (docker pins mounts at create)
|
||||
// — their /game-saves/<file> is a plain file inside the saves volume. For
|
||||
// those we self-verify: read the file back through the container, and if it
|
||||
// doesn't match the fresh render, copy the rendered bytes in directly
|
||||
// (works running or stopped; the game only reads config at boot).
|
||||
func (d *Dispatcher) handleRenderConfig(ctx context.Context, corrID string, req *panelv1.InstanceRenderConfigRequest) {
|
||||
log := d.log.With("instance_id", req.InstanceId, "correlation_id", corrID)
|
||||
fail := func(msg string) {
|
||||
d.sendEnv(&panelv1.AgentEnvelope{
|
||||
CorrelationId: corrID,
|
||||
SentAt: timestamppb.Now(),
|
||||
Payload: &panelv1.AgentEnvelope_InstanceRenderConfigResult{
|
||||
InstanceRenderConfigResult: &panelv1.InstanceRenderConfigResult{Error: msg},
|
||||
},
|
||||
})
|
||||
log.Warn("render config failed", "err", msg)
|
||||
}
|
||||
|
||||
rec, err := d.lookupRecord(req.InstanceId)
|
||||
if err != nil {
|
||||
fail(err.Error())
|
||||
return
|
||||
}
|
||||
if rec.DataPath == "" {
|
||||
fail("instance has no data path (pre-feature sidecar?) — recreate once to adopt")
|
||||
return
|
||||
}
|
||||
manifest, ok := d.modules.Get(rec.ModuleID)
|
||||
if !ok {
|
||||
fail("module " + rec.ModuleID + " not in registry")
|
||||
return
|
||||
}
|
||||
|
||||
// Same merge handleCreate uses: generated secrets first, user values on
|
||||
// top (empties dropped → template `or` defaults kick in).
|
||||
values, err := mergeValuesAndSecrets(manifest, req.ConfigValues)
|
||||
if err != nil {
|
||||
fail("secrets: " + err.Error())
|
||||
return
|
||||
}
|
||||
// Branch-aware: a 3.0 instance renders serverconfig.v3.xml.tmpl, ≤2.6
|
||||
// keeps the default. Resolve from the instance's persisted branch (set at
|
||||
// create), falling back to the merged config_values' provider_id so a
|
||||
// durable config-render still picks the right template if the record
|
||||
// predates branch persistence.
|
||||
branch := rec.Branch
|
||||
if branch == "" {
|
||||
branch = resolveCreateBranch(manifest, values)
|
||||
}
|
||||
if err := modulepkg.RenderForBranch(manifest, rec.DataPath, values, branch); err != nil {
|
||||
fail("render: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Self-verify through the container; fall back to a direct copy into
|
||||
// the volume for pre-bind containers. The mapping host-path → container
|
||||
// path comes from the manifest's volume specs.
|
||||
volumes := agentmodule.ResolveVolumes(manifest, rec.InstanceID, rec.DataPath)
|
||||
container := "panel-" + rec.InstanceID
|
||||
var rendered []string
|
||||
for _, cf := range manifest.ConfigFiles {
|
||||
if cf.TemplateForBranch(branch) == "" {
|
||||
continue
|
||||
}
|
||||
rendered = append(rendered, cf.Path)
|
||||
hostPath := filepath.Join(rec.DataPath, cf.Path)
|
||||
ctrPath := ""
|
||||
for _, v := range volumes {
|
||||
if v.Type == "bind" && v.HostPath == hostPath {
|
||||
ctrPath = v.ContainerPath
|
||||
break
|
||||
}
|
||||
}
|
||||
if ctrPath == "" {
|
||||
continue // file isn't mounted into the container; host render is all there is
|
||||
}
|
||||
want, rerr := os.ReadFile(hostPath)
|
||||
if rerr != nil {
|
||||
fail("read rendered " + cf.Path + ": " + rerr.Error())
|
||||
return
|
||||
}
|
||||
got, gerr := d.runtime.CopyFileFromContainer(ctx, container, ctrPath)
|
||||
if gerr == nil && bytes.Equal(got, want) {
|
||||
continue // live bind-mount delivered the new render
|
||||
}
|
||||
// Pre-bind container (or unreadable): write the volume copy directly.
|
||||
if cerr := d.runtime.CopyFileToContainer(ctx, container, ctrPath, want); cerr != nil {
|
||||
fail("container has no live bind for " + cf.Path + " and direct copy failed: " + cerr.Error())
|
||||
return
|
||||
}
|
||||
log.Info("rendered config copied into pre-bind container volume", "file", cf.Path, "container_path", ctrPath)
|
||||
}
|
||||
log.Info("re-rendered config files (durable, applies next boot)", "files", rendered)
|
||||
d.sendEnv(&panelv1.AgentEnvelope{
|
||||
CorrelationId: corrID,
|
||||
SentAt: timestamppb.Now(),
|
||||
Payload: &panelv1.AgentEnvelope_InstanceRenderConfigResult{
|
||||
InstanceRenderConfigResult: &panelv1.InstanceRenderConfigResult{RenderedFiles: rendered},
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user