feat(controller): env assembly from resource bindings for template agents
buildAgentEnvFromBindings resolves selected resource bindings into a flat env map for a template agent's .env at start: non-secret values from Metadata, secret values resolved from Key Vault (getJSONSecret) only here, lazily, never persisted. AM-independent foundation for the deploy-agent flow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/heicode/manager/common"
|
||||
"github.com/heicode/manager/model"
|
||||
)
|
||||
|
||||
// envMapEntry describes how one environment variable is sourced from a resource
|
||||
// binding when starting a template agent.
|
||||
//
|
||||
// Source == "metadata" -> value taken from the binding's non-secret Metadata JSON
|
||||
// Source == "secret" -> value resolved from Key Vault (binding.SecretRef) at
|
||||
// start time only; never persisted in plaintext.
|
||||
//
|
||||
// EnvMap on a ResourceBinding is a JSON object: {"ENV_NAME": {"source": "...",
|
||||
// "key": "<field in metadata or secret json>"}}.
|
||||
type envMapEntry struct {
|
||||
Source string `json:"source"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
// buildAgentEnvFromBindings resolves the selected resource bindings into a flat
|
||||
// environment map for a template agent's .env at start time.
|
||||
//
|
||||
// Non-secret values come from each binding's Metadata; secret values are read
|
||||
// from Key Vault here, at start time, and returned for injection into the agent
|
||||
// — they are NOT stored back on the Manager side. Callers must not log the
|
||||
// returned map.
|
||||
func buildAgentEnvFromBindings(userID int, bindingIDs []int) (map[string]string, error) {
|
||||
env := map[string]string{}
|
||||
if len(bindingIDs) == 0 {
|
||||
return env, nil
|
||||
}
|
||||
if model.DB == nil {
|
||||
return nil, fmt.Errorf("database not initialised")
|
||||
}
|
||||
|
||||
var store secretStoreClient
|
||||
storeReady := false
|
||||
|
||||
for _, id := range bindingIDs {
|
||||
var binding model.ResourceBinding
|
||||
if err := model.DB.Where("id = ? AND user_id = ?", id, userID).First(&binding).Error; err != nil {
|
||||
return nil, fmt.Errorf("resource binding %d not found for user", id)
|
||||
}
|
||||
|
||||
entries := map[string]envMapEntry{}
|
||||
if strings.TrimSpace(binding.EnvMap) != "" {
|
||||
if err := common.UnmarshalJsonStr(binding.EnvMap, &entries); err != nil {
|
||||
return nil, fmt.Errorf("binding %d env_map is not valid JSON: %w", id, err)
|
||||
}
|
||||
}
|
||||
if len(entries) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
metadata := map[string]any{}
|
||||
if strings.TrimSpace(binding.Metadata) != "" {
|
||||
_ = common.UnmarshalJsonStr(binding.Metadata, &metadata)
|
||||
}
|
||||
|
||||
// The secret JSON is fetched lazily, only if some entry needs it.
|
||||
var secret map[string]any
|
||||
secretLoaded := false
|
||||
|
||||
for envName, entry := range entries {
|
||||
name := strings.TrimSpace(envName)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
switch entry.Source {
|
||||
case "metadata":
|
||||
if value, ok := metadata[entry.Key]; ok {
|
||||
env[name] = fmt.Sprintf("%v", value)
|
||||
}
|
||||
case "secret":
|
||||
if !secretLoaded {
|
||||
if strings.TrimSpace(binding.SecretRef) == "" {
|
||||
return nil, fmt.Errorf("binding %d env_map needs a secret but the binding has no secret_ref", id)
|
||||
}
|
||||
if !storeReady {
|
||||
client, err := newSecretStoreClientFromEnv()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
store = client
|
||||
storeReady = true
|
||||
}
|
||||
resolved, err := store.getJSONSecret(binding.SecretRef)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("binding %d secret read failed: %w", id, err)
|
||||
}
|
||||
secret = resolved
|
||||
secretLoaded = true
|
||||
}
|
||||
if value, ok := secret[entry.Key]; ok {
|
||||
env[name] = fmt.Sprintf("%v", value)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("binding %d env_map %q: unknown source %q (want metadata|secret)", id, name, entry.Source)
|
||||
}
|
||||
}
|
||||
}
|
||||
return env, nil
|
||||
}
|
||||
Reference in New Issue
Block a user