|
|
|
@@ -1,8 +1,12 @@
|
|
|
|
|
// 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).
|
|
|
|
|
// service's POST /config endpoint over an ALWAYS-mTLS edge: Maestro's own
|
|
|
|
|
// identity (CN=maestro) and plain "https" regardless of dev mode, targeted at
|
|
|
|
|
// the service's dedicated peer listener port (label org.dcntrlized.peer_port,
|
|
|
|
|
// default 9443), which the service gates to CN=maestro. A set carries
|
|
|
|
|
// {field, value, type}; a clear carries {field, action:"clear"} — the service
|
|
|
|
|
// owns the schema and branches on what it receives.
|
|
|
|
|
//
|
|
|
|
|
// 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
|
|
|
|
@@ -23,6 +27,26 @@ import (
|
|
|
|
|
"git.dcntrlized.org/Dcntrlized/Maestro/lib/logger"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// peerPortLabel is the compose label a service declares to advertise the
|
|
|
|
|
// internal port of its always-mTLS Maestro peer listener.
|
|
|
|
|
const peerPortLabel = "org.dcntrlized.peer_port"
|
|
|
|
|
|
|
|
|
|
// defaultPeerPort is the fleet-wide default for that listener. Services keep/ve
|
|
|
|
|
// to it unless the label overrides it.
|
|
|
|
|
const defaultPeerPort = "9443"
|
|
|
|
|
|
|
|
|
|
// peerPort resolves the configured {peer listener port} from a container's
|
|
|
|
|
// compose labels, falling back to the fleet default.
|
|
|
|
|
func peerPort(labels map[string]string) string {
|
|
|
|
|
if labels == nil {
|
|
|
|
|
return defaultPeerPort
|
|
|
|
|
}
|
|
|
|
|
if p := labels[peerPortLabel]; p != "" {
|
|
|
|
|
return p
|
|
|
|
|
}
|
|
|
|
|
return defaultPeerPort
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ConfigDelivery is one key-value pair to deliver to a running service.
|
|
|
|
|
type ConfigDelivery struct {
|
|
|
|
|
Field string // manifest config field name
|
|
|
|
@@ -32,10 +56,12 @@ type ConfigDelivery struct {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// configRequest is the JSON body POSTed to the service's /config endpoint.
|
|
|
|
|
// Action is empty for a set (the existing contract) and "clear" for a clear.
|
|
|
|
|
type configRequest struct {
|
|
|
|
|
Field string `json:"field"`
|
|
|
|
|
Value string `json:"value"`
|
|
|
|
|
Type string `json:"type"`
|
|
|
|
|
Field string `json:"field"`
|
|
|
|
|
Value string `json:"value,omitempty"`
|
|
|
|
|
Type string `json:"type,omitempty"`
|
|
|
|
|
Action string `json:"action,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DeliverConfigValues delivers the given config pairs to the running service
|
|
|
|
@@ -46,47 +72,6 @@ func DeliverConfigValues(ctx context.Context, serviceName string, items []Config
|
|
|
|
|
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,
|
|
|
|
@@ -97,26 +82,81 @@ func DeliverConfigValues(ctx context.Context, serviceName string, items []Config
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
postConfig(ctx, serviceName, item.Field, "value", body)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DeliverConfigClear tells the running service to clear a config field. The
|
|
|
|
|
// service decides what "clear" means (drop the value, fall back to defaults);
|
|
|
|
|
// like sets, it is best-effort and covered by the next start.
|
|
|
|
|
func DeliverConfigClear(ctx context.Context, serviceName, field string) {
|
|
|
|
|
body, err := json.Marshal(configRequest{Field: field, Action: "clear"})
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.Warn("Config clear delivery to %s/%s: marshal failed: %v", serviceName, field, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
postConfig(ctx, serviceName, field, "clear", body)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// postConfig performs one best-effort POST of body to the running service's
|
|
|
|
|
// /config endpoint. NotFound/unreachable cases are logged at debug (the write
|
|
|
|
|
// still applies at next start); transport errors carry the field name only,
|
|
|
|
|
// never a value.
|
|
|
|
|
func postConfig(ctx context.Context, serviceName, field, verb string, body []byte) {
|
|
|
|
|
dc, err := docker.NewDockerClient()
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.Warn("Config delivery to %s/%s: cannot open docker client: %v", serviceName, field, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer dc.Close()
|
|
|
|
|
|
|
|
|
|
inspectData, err := docker.ResolveContainer(ctx, dc, serviceName)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.Debug("Config delivery to %s/%s: service not running or not found (%v); will apply at next start", serviceName, field, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
network, _, err := docker.ExtractNetworkInfo(ctx, dc, inspectData)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.Debug("Config delivery to %s/%s: cannot extract network info; will apply at next start", serviceName, field)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// /config is served on the service's dedicated Maestro peer listener
|
|
|
|
|
// (always mTLS, gated to CN=maestro), NOT the RN-facing flat edge. Maestro
|
|
|
|
|
// presents its own identity and targets the peer port; dev mode does not
|
|
|
|
|
// downgrade this edge.
|
|
|
|
|
port := peerPort(inspectData.Config.Labels)
|
|
|
|
|
|
|
|
|
|
transport, err := authmgmt.MaestroTransport()
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.Warn("Config delivery to %s/%s: cannot build Maestro peer transport: %v", serviceName, field, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
client := &http.Client{
|
|
|
|
|
Transport: transport,
|
|
|
|
|
Timeout: 5 * time.Second,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
url := fmt.Sprintf("https://%s:%s/config", 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, field, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
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); will apply at next start", serviceName, field, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
|
|
|
|
logger.Info("Delivered operator config %s/%s (%s) to running service", serviceName, field, verb)
|
|
|
|
|
} else {
|
|
|
|
|
logger.Debug("Config delivery to %s/%s: service responded %d; will apply at next start", serviceName, field, resp.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
}
|