Replace the user-facing env_map mistake with a built-in env convention keyed by resource type + provider — users never see/edit env names; they only fill plain resource fields. Supports git (gitea/github/gitlab), vm, database (mysql/pg/redis/mongo, with alias normalisation), storage (azure blob / bucket). Lenient: missing optional fields are skipped; only unsupported type or a KV read failure errors. Other issues found in review and fixed: - start timeout: template-agent start now uses a longer timeout (default 60s, AGENT_RUNTIME_START_TIMEOUT_SECONDS) since AM provisions synchronously — 5s would time out. amTemplateDo takes a per-call timeout. - orphan agent: if AM start succeeds but the Manager record fails to persist, the orphan is rolled back (best-effort amDeleteTemplateAgent). - findUserTemplateAgent now guards template_id<>'' so the new endpoints can't touch a legacy task deployment. - binding_ids defaults to [] (not null). - removed ResourceBinding.EnvMap field entirely. Tests rewritten for the built-in convention (blob metadata-only, db provider prefixes incl pg/mg aliases, git provider-agnostic names, ownership, unsupported type, empty); adapter round-trip + router tests still pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
161 lines
5.5 KiB
Go
161 lines
5.5 KiB
Go
package controller
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/heicode/manager/model"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestBuildAgentEnvFromBindings_BuiltinBlobMetadataOnly(t *testing.T) {
|
|
setupResourceControllerTestDB(t)
|
|
|
|
// No secret_ref -> secret fields (BLOB_KEY) are skipped, not an error.
|
|
b := model.ResourceBinding{
|
|
UserId: 7,
|
|
Name: "blob1",
|
|
ResourceType: "blob",
|
|
Provider: "blob",
|
|
Metadata: `{"account":"myacct","container":"uploads"}`,
|
|
}
|
|
require.NoError(t, model.DB.Create(&b).Error)
|
|
|
|
env, err := buildAgentEnvFromBindings(7, []int{b.Id})
|
|
require.NoError(t, err)
|
|
require.Equal(t, "myacct", env["BLOB_ACCOUNT"])
|
|
require.Equal(t, "uploads", env["BLOB_CONTAINER"])
|
|
_, hasKey := env["BLOB_KEY"]
|
|
require.False(t, hasKey) // no secret bound
|
|
}
|
|
|
|
func TestBuildAgentEnvFromBindings_DatabaseProviderPrefix(t *testing.T) {
|
|
setupResourceControllerTestDB(t)
|
|
cases := []struct {
|
|
provider string
|
|
wantHost string // env var that should carry the host
|
|
}{
|
|
{"mysql", "MYSQL_HOST"},
|
|
{"pg", "PG_HOST"},
|
|
{"postgresql", "PG_HOST"}, // alias normalises to pg
|
|
{"redis", "REDIS_HOST"},
|
|
{"mg", "MONGO_HOST"}, // alias normalises to mongo
|
|
}
|
|
for _, tc := range cases {
|
|
b := model.ResourceBinding{
|
|
UserId: 7,
|
|
Name: "db-" + tc.provider,
|
|
ResourceType: "database",
|
|
Provider: tc.provider,
|
|
Metadata: `{"host":"db.example","port":"5432","database":"app","user":"u"}`,
|
|
}
|
|
require.NoError(t, model.DB.Create(&b).Error)
|
|
env, err := buildAgentEnvFromBindings(7, []int{b.Id})
|
|
require.NoError(t, err, tc.provider)
|
|
require.Equal(t, "db.example", env[tc.wantHost], "provider %s -> %s", tc.provider, tc.wantHost)
|
|
}
|
|
}
|
|
|
|
func TestBuildAgentEnvFromBindings_GitNamesProviderAgnostic(t *testing.T) {
|
|
setupResourceControllerTestDB(t)
|
|
for _, provider := range []string{"github", "gitea", "gitlab"} {
|
|
b := model.ResourceBinding{
|
|
UserId: 7,
|
|
Name: "git-" + provider,
|
|
ResourceType: "git",
|
|
Provider: provider,
|
|
Metadata: `{"provider":"` + provider + `","repo_url":"https://x/owner/repo","default_branch":"main"}`,
|
|
}
|
|
require.NoError(t, model.DB.Create(&b).Error)
|
|
env, err := buildAgentEnvFromBindings(7, []int{b.Id})
|
|
require.NoError(t, err)
|
|
require.Equal(t, "https://x/owner/repo", env["GIT_REPO_URL"])
|
|
require.Equal(t, "main", env["GIT_BRANCH"])
|
|
require.Equal(t, provider, env["GIT_PROVIDER"])
|
|
}
|
|
}
|
|
|
|
func TestBuildAgentEnvFromBindings_OwnershipEnforced(t *testing.T) {
|
|
setupResourceControllerTestDB(t)
|
|
b := model.ResourceBinding{UserId: 7, Name: "x", ResourceType: "blob", Provider: "blob", Metadata: `{}`}
|
|
require.NoError(t, model.DB.Create(&b).Error)
|
|
|
|
_, err := buildAgentEnvFromBindings(99, []int{b.Id}) // different user
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestBuildAgentEnvFromBindings_UnsupportedType(t *testing.T) {
|
|
setupResourceControllerTestDB(t)
|
|
b := model.ResourceBinding{UserId: 7, Name: "x", ResourceType: "quantum", Provider: "weird", Metadata: `{}`}
|
|
require.NoError(t, model.DB.Create(&b).Error)
|
|
|
|
_, err := buildAgentEnvFromBindings(7, []int{b.Id})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestBuildAgentEnvFromBindings_Empty(t *testing.T) {
|
|
setupResourceControllerTestDB(t)
|
|
env, err := buildAgentEnvFromBindings(7, nil)
|
|
require.NoError(t, err)
|
|
require.Empty(t, env)
|
|
}
|
|
|
|
func TestAgentTemplatePathSubstitution(t *testing.T) {
|
|
require.Contains(t, agentTemplateStartPath("tpl1"), "/templates/tpl1/start")
|
|
require.Contains(t, agentTemplateAgentStopPath("rt-9"), "/agents/rt-9/stop")
|
|
require.Contains(t, agentTemplateAgentPath("rt-9"), "/agents/rt-9")
|
|
}
|
|
|
|
func TestAMStartTemplateAgent_RoundTrip(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
require.Equal(t, http.MethodPost, r.Method)
|
|
require.Equal(t, "/api/agent/templates/tpl1/start", r.URL.Path)
|
|
body, _ := io.ReadAll(r.Body)
|
|
require.Contains(t, string(body), "manager_deployment_id")
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"success":true,"data":{"runtime_id":"rt-9","subdomain":"https://x.example","access_token":"tok","status":"running"}}`))
|
|
}))
|
|
defer srv.Close()
|
|
t.Setenv("AGENT_RUNTIME_BASE_URL", srv.URL)
|
|
|
|
res, err := amStartTemplateAgent(context.Background(), "tpl1", "dep_1", map[string]string{"K": "V"}, "https://cb")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "rt-9", res.RuntimeID)
|
|
require.Equal(t, "https://x.example", res.Subdomain)
|
|
require.Equal(t, "tok", res.AccessToken)
|
|
require.Equal(t, "running", res.Status)
|
|
}
|
|
|
|
func TestAMGetAgentStatus_RoundTrip(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
require.Equal(t, "/api/agent/agents/rt-1", r.URL.Path)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"success":true,"data":{"status":"running"}}`))
|
|
}))
|
|
defer srv.Close()
|
|
t.Setenv("AGENT_RUNTIME_BASE_URL", srv.URL)
|
|
|
|
status, err := amGetAgentStatus(context.Background(), "rt-1")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "running", status)
|
|
}
|
|
|
|
func TestTemplateAgentResponse(t *testing.T) {
|
|
row := model.AgentDeployment{
|
|
DeploymentID: "dep_abc",
|
|
TemplateID: "tpl1",
|
|
Subdomain: "https://abc.agents.example",
|
|
AccessToken: "tok",
|
|
BindingIDsJSON: "[1,2]",
|
|
Status: "running",
|
|
}
|
|
resp := templateAgentResponse(row)
|
|
require.Equal(t, "dep_abc", resp["agent_id"])
|
|
require.Equal(t, "tpl1", resp["template_id"])
|
|
require.Equal(t, []int{1, 2}, resp["binding_ids"])
|
|
require.Equal(t, "running", resp["status"])
|
|
}
|