582b5a6b08
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
126 lines
4.7 KiB
Go
126 lines
4.7 KiB
Go
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},
|
|
},
|
|
})
|
|
}
|