feat(config): fail-closed manifest-driven injection + runtime write-back delivery (D6)

This commit is contained in:
2026-08-31 09:43:43 +01:00
parent a9e2182396
commit c540ad5d7f
6 changed files with 326 additions and 16 deletions
+122
View File
@@ -0,0 +1,122 @@
// Package containers implements Maestro's runtime write-back delivery for the
// Service Config & Secrets theme (D6). When the operator sets or clears a
// config value via the Settings panel, Maestro delivers it to the running
// service's POST /config endpoint over the existing mTLS peer edge (same
// transport as /action/* and manifest fetch).
//
// Delivery is best-effort: failures are logged (with the field name, never
// the value) and do not prevent the panel write from succeeding. Values always
// also take effect at the next StartContainer via injectConfigSecrets (M1),
// so delivery is an accelerator, not the sole path.
package containers
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"git.dcntrlized.org/Dcntrlized/Maestro/lib/authmgmt"
"git.dcntrlized.org/Dcntrlized/Maestro/lib/docker"
"git.dcntrlized.org/Dcntrlized/Maestro/lib/logger"
)
// ConfigDelivery is one key-value pair to deliver to a running service.
type ConfigDelivery struct {
Field string // manifest config field name
Key string // container env var name
Type string // "cleartext" | "password"
Value string // the operator-entered value (sent over mTLS; never logged)
}
// configRequest is the JSON body POSTed to the service's /config endpoint.
type configRequest struct {
Field string `json:"field"`
Value string `json:"value"`
Type string `json:"type"`
}
// DeliverConfigValues delivers the given config pairs to the running service
// over the mTLS peer edge. If the service is not running or unreachable, the
// error is logged but not returned — the values will take effect at next start
// via injectConfigSecrets.
func DeliverConfigValues(ctx context.Context, serviceName string, items []ConfigDelivery) {
if len(items) == 0 {
return
}
dc, err := docker.NewDockerClient()
if err != nil {
logger.Warn("Config delivery to %s: cannot open docker client: %v", serviceName, err)
return
}
defer dc.Close()
inspectData, err := docker.ResolveContainer(ctx, dc, serviceName)
if err != nil {
logger.Debug("Config delivery to %s: service not running or not found (%v); values will apply at next start", serviceName, err)
return
}
shouldCache := false
network, _, err := docker.ExtractNetworkInfo(ctx, dc, inspectData)
if err != nil || !shouldCache {
logger.Debug("Config delivery to %s: cannot extract network info; values will apply at next start", serviceName)
return
}
port := inspectData.Config.Labels["org.dcntrlized.port"]
if port == "" && len(network.Ports) > 0 {
port = fmt.Sprintf("%d", network.Ports[0].Guest)
}
if port == "" {
logger.Debug("Config delivery to %s: no service port; values will apply at next start", serviceName)
return
}
transport, err := authmgmt.PeerTransport()
if err != nil {
logger.Warn("Config delivery to %s: cannot build peer transport: %v", serviceName, err)
return
}
client := &http.Client{
Transport: transport,
Timeout: 5 * time.Second,
}
for _, item := range items {
body, err := json.Marshal(configRequest{
Field: item.Field,
Value: item.Value,
Type: item.Type,
})
if err != nil {
logger.Warn("Config delivery to %s/%s: marshal failed: %v", serviceName, item.Field, err)
continue
}
url := fmt.Sprintf("%s://%s:%s/config", authmgmt.Scheme(), network.IPAddress.Address, port)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
logger.Warn("Config delivery to %s/%s: request build failed: %v", serviceName, item.Field, err)
continue
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
logger.Debug("Config delivery to %s/%s: service unreachable (%v); value will apply at next start", serviceName, item.Field, err)
continue
}
resp.Body.Close()
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
logger.Info("Delivered operator config %s/%s to running service %s", serviceName, item.Field, serviceName)
} else {
logger.Debug("Config delivery to %s/%s: service responded %d; value will apply at next start", serviceName, item.Field, resp.StatusCode)
}
}
}