e75c8b37c8
Co-authored-by: nova28 <17953305+nova28@users.noreply.github.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
251 lines
10 KiB
Go
251 lines
10 KiB
Go
package backendapp
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jmoiron/sqlx"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/kandev/kandev/internal/agent/agents"
|
|
agentexecutor "github.com/kandev/kandev/internal/agent/executor"
|
|
"github.com/kandev/kandev/internal/agent/registry"
|
|
"github.com/kandev/kandev/internal/agent/runtime/lifecycle"
|
|
"github.com/kandev/kandev/internal/agentctl/server/process"
|
|
"github.com/kandev/kandev/internal/db"
|
|
"github.com/kandev/kandev/internal/events/bus"
|
|
orchestratorexecutor "github.com/kandev/kandev/internal/orchestrator/executor"
|
|
"github.com/kandev/kandev/internal/task/models"
|
|
sqlitetaskrepo "github.com/kandev/kandev/internal/task/repository/sqlite"
|
|
v1 "github.com/kandev/kandev/pkg/api/v1"
|
|
)
|
|
|
|
var errKubernetesLaunchCaptured = errors.New("kubernetes launch captured")
|
|
|
|
type capturingKubernetesBackend struct {
|
|
req *lifecycle.ExecutorCreateRequest
|
|
}
|
|
|
|
func (b *capturingKubernetesBackend) Name() agentexecutor.Name { return agentexecutor.NameKubernetes }
|
|
func (b *capturingKubernetesBackend) HealthCheck(context.Context) error { return nil }
|
|
func (b *capturingKubernetesBackend) CreateInstance(
|
|
_ context.Context,
|
|
req *lifecycle.ExecutorCreateRequest,
|
|
) (*lifecycle.ExecutorInstance, error) {
|
|
b.req = req
|
|
return nil, errKubernetesLaunchCaptured
|
|
}
|
|
func (b *capturingKubernetesBackend) StopInstance(context.Context, *lifecycle.ExecutorInstance, bool) error {
|
|
return nil
|
|
}
|
|
func (b *capturingKubernetesBackend) RecoverInstances(context.Context, []*models.ExecutorRunning) ([]*lifecycle.ExecutorInstance, error) {
|
|
return nil, nil
|
|
}
|
|
func (b *capturingKubernetesBackend) GetInteractiveRunner() *process.InteractiveRunner { return nil }
|
|
func (b *capturingKubernetesBackend) RequiresCloneURL() bool { return true }
|
|
func (b *capturingKubernetesBackend) ShouldApplyPreferredShell() bool { return false }
|
|
func (b *capturingKubernetesBackend) IsAlwaysResumable() bool { return true }
|
|
|
|
type kubernetesLaunchProfileResolver struct{}
|
|
|
|
func (kubernetesLaunchProfileResolver) ResolveProfile(
|
|
context.Context,
|
|
string,
|
|
) (*lifecycle.AgentProfileInfo, error) {
|
|
return &lifecycle.AgentProfileInfo{
|
|
ProfileID: "agent-profile", AgentID: "mock-agent", AgentName: "mock-agent",
|
|
}, nil
|
|
}
|
|
|
|
type kubernetesLaunchProfileReader struct{}
|
|
|
|
func (kubernetesLaunchProfileReader) GetTask(context.Context, string) (*models.Task, error) {
|
|
return &models.Task{ID: "task-1"}, nil
|
|
}
|
|
|
|
func (kubernetesLaunchProfileReader) GetTaskSession(context.Context, string) (*models.TaskSession, error) {
|
|
return &models.TaskSession{ID: "session-1", TaskID: "task-1", ExecutorProfileID: "profile-k8s"}, nil
|
|
}
|
|
func (kubernetesLaunchProfileReader) HasActiveTaskResourceCleanupJob(context.Context, string) (bool, error) {
|
|
return false, nil
|
|
}
|
|
func (kubernetesLaunchProfileReader) GetTaskEnvironment(context.Context, string) (*models.TaskEnvironment, error) {
|
|
return nil, nil
|
|
}
|
|
func (kubernetesLaunchProfileReader) GetExecutorProfile(context.Context, string) (*models.ExecutorProfile, error) {
|
|
return &models.ExecutorProfile{
|
|
ID: "profile-k8s", ExecutorID: "executor-k8s",
|
|
Config: map[string]string{
|
|
lifecycle.MetadataKeyKubernetesProfilePlatform: "linux/amd64",
|
|
lifecycle.MetadataKeyKubernetesProfileMainContainer: "kandev-agent",
|
|
lifecycle.MetadataKeyKubernetesPodTemplateYAML: "apiVersion: v1\nkind: PodTemplate\ntemplate:\n spec:\n containers:\n - name: kandev-agent\n image: example.test/agent:latest\n",
|
|
lifecycle.MetadataKeyKubernetesWorkspaceMode: "empty_dir",
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// Reviewer-requested boundary contract: orchestrator request -> backend adapter
|
|
// -> lifecycle runtime request must keep authoritative Kubernetes connection keys.
|
|
func TestKubernetesLaunchCarriesConnectionConfigThroughAdapterAndLifecycle(t *testing.T) {
|
|
log := newTestLogger()
|
|
agentRegistry := registry.NewRegistry(log)
|
|
mockAgent := agents.NewMockAgentWithID("mock-agent", "Mock", "Mock")
|
|
mockAgent.SetEnabled(true)
|
|
require.NoError(t, agentRegistry.Register(mockAgent))
|
|
backend := &capturingKubernetesBackend{}
|
|
executorRegistry := lifecycle.NewExecutorRegistry(log)
|
|
executorRegistry.Register(backend)
|
|
eventBus := bus.NewMemoryEventBus(log)
|
|
t.Cleanup(eventBus.Close)
|
|
mgr := lifecycle.NewManager(
|
|
agentRegistry, eventBus, executorRegistry, nil, kubernetesLaunchProfileResolver{}, nil,
|
|
lifecycle.ExecutorFallbackWarn, "", log,
|
|
)
|
|
mgr.SetExecutorProfileReader(kubernetesLaunchProfileReader{})
|
|
t.Cleanup(func() { require.NoError(t, mgr.Stop()) })
|
|
adapter := newLifecycleAdapter(mgr, agentRegistry, log)
|
|
wantConfig := map[string]string{
|
|
lifecycle.MetadataKeyKubernetesAuthMode: "kubeconfig",
|
|
lifecycle.MetadataKeyKubernetesKubeconfigPath: "/etc/kandev/kind.yaml",
|
|
lifecycle.MetadataKeyKubernetesKubeContext: "kind-kandev",
|
|
lifecycle.MetadataKeyKubernetesConfigNamespace: "kandev-agents",
|
|
lifecycle.MetadataKeyKubernetesRequestTimeoutSeconds: "45",
|
|
}
|
|
|
|
_, err := adapter.LaunchAgent(context.Background(), &orchestratorexecutor.LaunchAgentRequest{
|
|
TaskID: "task-1", WorkspaceID: "workspace-1", SessionID: "session-1",
|
|
AgentProfileID: "agent-profile", ExecutorType: string(models.ExecutorTypeKubernetes),
|
|
ExecutorConfig: wantConfig,
|
|
Metadata: map[string]interface{}{
|
|
lifecycle.MetadataKeyExecutorProfileID: "profile-k8s",
|
|
"executor_id": "executor-k8s",
|
|
},
|
|
})
|
|
|
|
require.ErrorIs(t, err, errKubernetesLaunchCaptured)
|
|
require.NotNil(t, backend.req)
|
|
for key, want := range wantConfig {
|
|
require.Equal(t, want, backend.req.Metadata[key], key)
|
|
}
|
|
}
|
|
|
|
// Reviewer-requested full-path contract: persisted executor selection must
|
|
// survive orchestrator request construction, backend adapter conversion, and
|
|
// lifecycle metadata projection before the Kubernetes client is constructed.
|
|
func TestPreparedKubernetesLaunchCarriesPersistedConnectionConfigToLifecycle(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
executorConfig map[string]string
|
|
wantMetadata map[string]string
|
|
}{
|
|
{
|
|
name: "kubeconfig",
|
|
executorConfig: map[string]string{
|
|
lifecycle.MetadataKeyKubernetesAuthMode: "kubeconfig",
|
|
lifecycle.MetadataKeyKubernetesKubeconfigPath: "/etc/kandev/kind.yaml",
|
|
lifecycle.MetadataKeyKubernetesKubeContext: "kind-kandev",
|
|
lifecycle.MetadataKeyKubernetesConfigNamespace: "kandev-agents",
|
|
lifecycle.MetadataKeyKubernetesRequestTimeoutSeconds: "45",
|
|
},
|
|
},
|
|
{
|
|
name: "in cluster clears kubeconfig fields",
|
|
executorConfig: map[string]string{
|
|
lifecycle.MetadataKeyKubernetesAuthMode: "in_cluster",
|
|
lifecycle.MetadataKeyKubernetesConfigNamespace: "kandev-agents",
|
|
lifecycle.MetadataKeyKubernetesRequestTimeoutSeconds: "30",
|
|
},
|
|
wantMetadata: map[string]string{
|
|
lifecycle.MetadataKeyKubernetesKubeconfigPath: "",
|
|
lifecycle.MetadataKeyKubernetesKubeContext: "",
|
|
},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
wantMetadata := make(map[string]string, len(test.wantMetadata)+len(test.executorConfig))
|
|
for key, value := range test.wantMetadata {
|
|
wantMetadata[key] = value
|
|
}
|
|
for key, value := range test.executorConfig {
|
|
wantMetadata[key] = value
|
|
}
|
|
testPreparedKubernetesLaunchCarriesConnectionConfig(
|
|
t, test.executorConfig, wantMetadata,
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testPreparedKubernetesLaunchCarriesConnectionConfig(
|
|
t *testing.T,
|
|
executorConfig map[string]string,
|
|
wantMetadata map[string]string,
|
|
) {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
dbConn, err := db.OpenSQLite(filepath.Join(t.TempDir(), "kubernetes-launch.db"))
|
|
require.NoError(t, err)
|
|
sqlxDB := sqlx.NewDb(dbConn, "sqlite3")
|
|
t.Cleanup(func() { require.NoError(t, sqlxDB.Close()) })
|
|
repo, err := sqlitetaskrepo.NewWithDB(sqlxDB, sqlxDB, nil)
|
|
require.NoError(t, err)
|
|
require.NoError(t, repo.CreateWorkspace(ctx, &models.Workspace{ID: "workspace-1", Name: "Workspace"}))
|
|
require.NoError(t, repo.CreateTask(ctx, &models.Task{
|
|
ID: "task-1", WorkspaceID: "workspace-1", Title: "Kubernetes launch", State: v1.TaskStateCreated,
|
|
}))
|
|
require.NoError(t, repo.CreateExecutor(ctx, &models.Executor{
|
|
ID: "executor-k8s", Name: "Kind", Type: models.ExecutorTypeKubernetes,
|
|
Status: models.ExecutorStatusActive, Config: executorConfig,
|
|
}))
|
|
require.NoError(t, repo.CreateExecutorProfile(ctx, &models.ExecutorProfile{
|
|
ID: "profile-k8s", ExecutorID: "executor-k8s", Name: "Kind workload",
|
|
Config: map[string]string{
|
|
lifecycle.MetadataKeyKubernetesProfilePlatform: "linux/amd64",
|
|
lifecycle.MetadataKeyKubernetesProfileMainContainer: "kandev-agent",
|
|
lifecycle.MetadataKeyKubernetesPodTemplateYAML: "apiVersion: v1\nkind: PodTemplate\ntemplate:\n spec:\n containers:\n - name: kandev-agent\n image: example.test/agent:latest\n",
|
|
lifecycle.MetadataKeyKubernetesWorkspaceMode: "empty_dir",
|
|
},
|
|
}))
|
|
now := time.Now().UTC()
|
|
require.NoError(t, repo.CreateTaskSession(ctx, &models.TaskSession{
|
|
ID: "session-1", TaskID: "task-1", AgentProfileID: "agent-profile",
|
|
ExecutorID: "executor-k8s", ExecutorProfileID: "profile-k8s",
|
|
State: models.TaskSessionStateCreated, StartedAt: now, UpdatedAt: now,
|
|
}))
|
|
|
|
log := newTestLogger()
|
|
agentRegistry := registry.NewRegistry(log)
|
|
mockAgent := agents.NewMockAgentWithID("mock-agent", "Mock", "Mock")
|
|
mockAgent.SetEnabled(true)
|
|
require.NoError(t, agentRegistry.Register(mockAgent))
|
|
backend := &capturingKubernetesBackend{}
|
|
executorRegistry := lifecycle.NewExecutorRegistry(log)
|
|
executorRegistry.Register(backend)
|
|
eventBus := bus.NewMemoryEventBus(log)
|
|
t.Cleanup(eventBus.Close)
|
|
mgr := lifecycle.NewManager(
|
|
agentRegistry, eventBus, executorRegistry, nil, kubernetesLaunchProfileResolver{}, nil,
|
|
lifecycle.ExecutorFallbackWarn, "", log,
|
|
)
|
|
mgr.SetExecutorProfileReader(repo)
|
|
t.Cleanup(func() { require.NoError(t, mgr.Stop()) })
|
|
adapter := newLifecycleAdapter(mgr, agentRegistry, log)
|
|
orchestrator := orchestratorexecutor.NewExecutor(
|
|
adapter, repo, log, orchestratorexecutor.ExecutorConfig{},
|
|
)
|
|
|
|
_, err = orchestrator.LaunchPreparedSession(ctx, &v1.Task{
|
|
ID: "task-1", WorkspaceID: "workspace-1", Title: "Kubernetes launch",
|
|
}, "session-1", orchestratorexecutor.LaunchOptions{AgentProfileID: "agent-profile"})
|
|
|
|
require.ErrorIs(t, err, errKubernetesLaunchCaptured)
|
|
require.NotNil(t, backend.req)
|
|
for key, want := range wantMetadata {
|
|
require.Equal(t, want, backend.req.Metadata[key], key)
|
|
}
|
|
}
|