49177d742f
Co-authored-by: ayattara <alassane.yattara@savoirfairelinux.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
234 lines
7.3 KiB
Go
234 lines
7.3 KiB
Go
package github
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/kandev/kandev/internal/events"
|
|
"github.com/kandev/kandev/internal/events/bus"
|
|
taskmodels "github.com/kandev/kandev/internal/task/models"
|
|
)
|
|
|
|
// subscribeTaskEvents wires task/workspace lifecycle cleanup. PR watches are
|
|
// pruned proactively, while task, session, and workspace credential leases are
|
|
// revoked as soon as their owning runtime scope terminates. Subscriptions are
|
|
// tracked so they can be released via unsubscribeTaskEvents.
|
|
func (s *Service) subscribeTaskEvents() {
|
|
if s.eventBus == nil {
|
|
return
|
|
}
|
|
if sub, err := s.eventBus.Subscribe(events.TaskUpdated, s.handleTaskUpdated); err != nil {
|
|
s.logger.Error("failed to subscribe to task.updated events", zap.Error(err))
|
|
} else {
|
|
s.taskEventSubs = append(s.taskEventSubs, sub)
|
|
}
|
|
if sub, err := s.eventBus.Subscribe(events.TaskDeleted, s.handleTaskDeleted); err != nil {
|
|
s.logger.Error("failed to subscribe to task.deleted events", zap.Error(err))
|
|
} else {
|
|
s.taskEventSubs = append(s.taskEventSubs, sub)
|
|
}
|
|
if sub, err := s.eventBus.Subscribe(events.WorkspaceDeleted, s.handleWorkspaceDeleted); err != nil {
|
|
s.logger.Error("failed to subscribe to workspace.deleted events", zap.Error(err))
|
|
} else {
|
|
s.taskEventSubs = append(s.taskEventSubs, sub)
|
|
}
|
|
if sub, err := s.eventBus.Subscribe(events.TaskSessionStateChanged, s.handleTaskSessionStateChanged); err != nil {
|
|
s.logger.Error("failed to subscribe to task session state events", zap.Error(err))
|
|
} else {
|
|
s.taskEventSubs = append(s.taskEventSubs, sub)
|
|
}
|
|
}
|
|
|
|
// unsubscribeTaskEvents releases the subscriptions created in subscribeTaskEvents.
|
|
// Called from the provider cleanup so handlers don't accumulate if the service is
|
|
// torn down and re-created while the event bus persists.
|
|
func (s *Service) unsubscribeTaskEvents() {
|
|
for _, sub := range s.taskEventSubs {
|
|
if err := sub.Unsubscribe(); err != nil {
|
|
s.logger.Error("failed to unsubscribe from task event", zap.Error(err))
|
|
}
|
|
}
|
|
s.taskEventSubs = nil
|
|
}
|
|
|
|
// handleTaskUpdated deletes PR watches when a task is archived. Non-archive
|
|
// updates are ignored so we don't interfere with normal task edits.
|
|
func (s *Service) handleTaskUpdated(ctx context.Context, event *bus.Event) error {
|
|
taskID, archived := taskIDAndArchivedFrom(event)
|
|
if taskID == "" || !archived {
|
|
return nil
|
|
}
|
|
s.revokeCredentialTask(taskID)
|
|
s.pruneWatchesForTask(ctx, taskID, "archived")
|
|
return nil
|
|
}
|
|
|
|
// handleTaskDeleted removes task-owned GitHub state when a task is hard-deleted.
|
|
func (s *Service) handleTaskDeleted(ctx context.Context, event *bus.Event) error {
|
|
taskID, _ := taskIDAndArchivedFrom(event)
|
|
if taskID == "" {
|
|
return nil
|
|
}
|
|
s.revokeCredentialTask(taskID)
|
|
s.pruneTaskOwnedStateForTask(ctx, taskID, "deleted")
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) handleTaskSessionStateChanged(_ context.Context, event *bus.Event) error {
|
|
sessionID, terminal := terminalSessionIDFrom(event)
|
|
if sessionID == "" || !terminal {
|
|
return nil
|
|
}
|
|
s.mu.Lock()
|
|
broker := s.credentialBroker
|
|
s.mu.Unlock()
|
|
if broker != nil {
|
|
broker.RevokeSession(sessionID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type connectionSecretIDLister interface {
|
|
ListIDs(context.Context) ([]string, error)
|
|
}
|
|
|
|
// handleWorkspaceDeleted removes workspace-owned GitHub metadata and encrypted
|
|
// credentials after the task repository deletes the workspace row. Secret IDs
|
|
// are namespaced, so future multi-user connections are covered without reading
|
|
// deleted user-connection rows.
|
|
func (s *Service) handleWorkspaceDeleted(ctx context.Context, event *bus.Event) error {
|
|
workspaceID := workspaceIDFromEvent(event)
|
|
if workspaceID == "" {
|
|
return nil
|
|
}
|
|
s.mu.Lock()
|
|
broker := s.credentialBroker
|
|
s.mu.Unlock()
|
|
if broker != nil {
|
|
broker.RevokeWorkspace(workspaceID)
|
|
}
|
|
if s.store != nil {
|
|
if err := s.store.DeleteWorkspaceSettings(ctx, workspaceID); err != nil {
|
|
return fmt.Errorf("delete GitHub workspace settings: %w", err)
|
|
}
|
|
}
|
|
if s.connectionSecrets == nil {
|
|
return nil
|
|
}
|
|
keys := []string{WorkspacePATSecretKey(workspaceID)}
|
|
if lister, ok := s.connectionSecrets.(connectionSecretIDLister); ok {
|
|
ids, err := lister.ListIDs(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("list GitHub connection secrets: %w", err)
|
|
}
|
|
userPrefix := "github:user:" + workspaceID + ":"
|
|
for _, id := range ids {
|
|
if strings.HasPrefix(id, userPrefix) {
|
|
keys = append(keys, id)
|
|
}
|
|
}
|
|
} else {
|
|
keys = append(keys,
|
|
UserAccessTokenSecretKey(workspaceID, DefaultUserID),
|
|
UserRefreshTokenSecretKey(workspaceID, DefaultUserID),
|
|
)
|
|
}
|
|
for _, key := range keys {
|
|
if err := deleteOptionalSecret(ctx, s.connectionSecrets, key); err != nil {
|
|
return fmt.Errorf("delete GitHub connection secret %q: %w", key, err)
|
|
}
|
|
}
|
|
s.invalidateWorkspaceCredential(workspaceID)
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) revokeCredentialTask(taskID string) {
|
|
s.mu.Lock()
|
|
broker := s.credentialBroker
|
|
s.mu.Unlock()
|
|
if broker != nil {
|
|
broker.RevokeTask(taskID)
|
|
}
|
|
}
|
|
|
|
func (s *Service) pruneTaskOwnedStateForTask(ctx context.Context, taskID, reason string) {
|
|
n, err := s.store.DeleteTaskOwnedStateByTaskID(ctx, taskID)
|
|
if err != nil {
|
|
s.logger.Error("failed to delete task-owned GitHub state", zap.String("task_id", taskID), zap.String("reason", reason), zap.Error(err))
|
|
return
|
|
}
|
|
if n > 0 {
|
|
s.logger.Info("pruned task-owned GitHub state after task change", zap.String("task_id", taskID), zap.String("reason", reason), zap.Int64("deleted", n))
|
|
}
|
|
}
|
|
|
|
func (s *Service) pruneWatchesForTask(ctx context.Context, taskID, reason string) {
|
|
n, err := s.store.DeletePRWatchesByTaskID(ctx, taskID)
|
|
if err != nil {
|
|
s.logger.Error("failed to delete PR watches for task",
|
|
zap.String("task_id", taskID),
|
|
zap.String("reason", reason),
|
|
zap.Error(err))
|
|
return
|
|
}
|
|
if n > 0 {
|
|
s.logger.Info("pruned PR watches after task change",
|
|
zap.String("task_id", taskID),
|
|
zap.String("reason", reason),
|
|
zap.Int64("deleted", n))
|
|
}
|
|
}
|
|
|
|
// taskIDAndArchivedFrom extracts task_id + archived flag from a task event
|
|
// payload (task-service publishes a map[string]interface{} — see
|
|
// internal/task/service/service_events.go publishTaskEvent). Archived is
|
|
// detected from a non-empty string value so a future null/zero archived_at
|
|
// in the payload doesn't silently prune watches on non-archive updates.
|
|
func taskIDAndArchivedFrom(event *bus.Event) (taskID string, archived bool) {
|
|
if event == nil {
|
|
return "", false
|
|
}
|
|
data, ok := event.Data.(map[string]interface{})
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
id, _ := data["task_id"].(string)
|
|
archivedStr, _ := data["archived_at"].(string)
|
|
return id, archivedStr != ""
|
|
}
|
|
|
|
func workspaceIDFromEvent(event *bus.Event) string {
|
|
if event == nil {
|
|
return ""
|
|
}
|
|
data, ok := event.Data.(map[string]interface{})
|
|
if !ok {
|
|
return ""
|
|
}
|
|
workspaceID, _ := data["id"].(string)
|
|
return strings.TrimSpace(workspaceID)
|
|
}
|
|
|
|
func terminalSessionIDFrom(event *bus.Event) (string, bool) {
|
|
if event == nil {
|
|
return "", false
|
|
}
|
|
data, ok := event.Data.(map[string]interface{})
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
sessionID, _ := data["session_id"].(string)
|
|
state, _ := data["new_state"].(string)
|
|
switch taskmodels.TaskSessionState(strings.ToUpper(strings.TrimSpace(state))) {
|
|
case taskmodels.TaskSessionStateCompleted,
|
|
taskmodels.TaskSessionStateFailed,
|
|
taskmodels.TaskSessionStateCancelled:
|
|
return strings.TrimSpace(sessionID), true
|
|
default:
|
|
return strings.TrimSpace(sessionID), false
|
|
}
|
|
}
|