feat(config): fail-closed manifest-driven injection + runtime write-back delivery (D6)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user