Files
kandev/apps/backend/internal/gitlab/provider.go
T
2026-08-15 17:21:14 +01:00

185 lines
5.5 KiB
Go

package gitlab
import (
"context"
"errors"
"fmt"
"os"
"strings"
"time"
"github.com/kandev/kandev/internal/common/logger"
"github.com/kandev/kandev/internal/integrations/workspacescope"
)
// Provide builds the GitLab service stack: discovers a host (via the
// optional HostStore, falling back to DefaultHost), resolves the best
// available client, and returns a *Service plus a cleanup function.
//
// secrets is required for PAT-fallback auth; passing nil disables it.
// hostStore is optional — when present, the persisted host is used
// instead of DefaultHost.
func Provide(
ctx context.Context,
secrets SecretProvider,
hostStore HostStore,
log *logger.Logger,
) (*Service, func() error, error) {
host := DefaultHost
if hostStore != nil {
persisted, err := hostStore.GetHost(ctx)
if err != nil {
return nil, nil, fmt.Errorf("load GitLab host: %w", err)
}
if persisted != "" {
host = persisted
}
}
client, authMethod, err := NewClient(ctx, host, secrets, log)
if err != nil {
log.Warn("GitLab client not available: " + err.Error())
}
svc := NewService(host, client, authMethod, secrets, log)
if hostStore != nil {
svc.SetHostStore(hostStore)
}
cleanup := func() error {
svc.unsubscribeTaskEvents()
return nil
}
return svc, cleanup, nil
}
// MigrateLegacyConnection moves the installation-wide host/token onto the
// deterministic active-or-earliest workspace. Existing workspace config wins.
func MigrateLegacyConnection(ctx context.Context, store *Store, workspaceSecrets WorkspaceSecretStore, legacySecrets SecretProvider, legacyManager SecretManager, hostStore HostStore, log *logger.Logger) error {
if store == nil {
return nil
}
target, err := workspacescope.ResolveMigrationTarget(store.db)
if err != nil {
return err
}
existing, err := store.GetConfigForWorkspace(ctx, target)
if err != nil || existing != nil {
return err
}
legacy, err := loadLegacyConnection(ctx, hostStore, legacySecrets, workspaceSecrets)
if err != nil {
return err
}
if legacy == nil {
return nil
}
var previousSecret workspaceSecretSnapshot
if legacy.token != "" {
previousSecret, err = snapshotWorkspaceSecret(ctx, workspaceSecrets, SecretKeyForWorkspace(target))
if err != nil {
return fmt.Errorf("snapshot migrated GitLab token: %w", err)
}
if err := workspaceSecrets.Set(ctx, SecretKeyForWorkspace(target), "GitLab token", legacy.token); err != nil {
return restoreLegacyWorkspaceSecret(ctx, workspaceSecrets, target, previousSecret, err)
}
}
cfg := &GitLabConfig{Host: legacy.host, AuthMethod: legacy.authMethod, CreatedAt: time.Now().UTC()}
if err := store.UpsertConfigForWorkspace(ctx, target, cfg); err != nil {
if legacy.token != "" {
return restoreLegacyWorkspaceSecret(ctx, workspaceSecrets, target, previousSecret, err)
}
return err
}
cleanupLegacyConnection(ctx, legacy.tokenID, legacyManager, hostStore, log)
return nil
}
func restoreLegacyWorkspaceSecret(ctx context.Context, secrets WorkspaceSecretStore, workspaceID string, snapshot workspaceSecretSnapshot, cause error) error {
rollbackCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
defer cancel()
if err := restoreWorkspaceSecret(rollbackCtx, secrets, SecretKeyForWorkspace(workspaceID), snapshot); err != nil {
return errors.Join(cause, fmt.Errorf("restore migrated GitLab token: %w", err))
}
return cause
}
type legacyConnection struct {
host string
authMethod string
token string
tokenID string
}
func loadLegacyConnection(ctx context.Context, hostStore HostStore, legacySecrets SecretProvider, workspaceSecrets WorkspaceSecretStore) (*legacyConnection, error) {
host, err := loadLegacyHost(ctx, hostStore)
if err != nil {
return nil, err
}
token, tokenID, err := findLegacyPAT(ctx, legacySecrets)
if err != nil {
return nil, err
}
environmentToken := strings.TrimSpace(os.Getenv(secretNameToken))
if host == "" && token == "" && environmentToken == "" {
return nil, nil
}
host, err = normalizeHostOrigin(host)
if err != nil {
return nil, err
}
authMethod, err := legacyAuthMethod(token, environmentToken, workspaceSecrets)
if err != nil {
return nil, err
}
return &legacyConnection{host: host, authMethod: authMethod, token: token, tokenID: tokenID}, nil
}
func loadLegacyHost(ctx context.Context, hostStore HostStore) (string, error) {
if hostStore == nil {
return "", nil
}
return hostStore.GetHost(ctx)
}
func legacyAuthMethod(token, environmentToken string, workspaceSecrets WorkspaceSecretStore) (string, error) {
if token != "" {
if workspaceSecrets == nil {
return "", errors.New("gitlab workspace secret store is required to migrate a PAT")
}
return AuthMethodPAT, nil
}
if environmentToken != "" {
return AuthMethodEnvironment, nil
}
return AuthMethodGLab, nil
}
func cleanupLegacyConnection(ctx context.Context, tokenID string, legacyManager SecretManager, hostStore HostStore, log *logger.Logger) {
if tokenID != "" && legacyManager != nil {
if err := legacyManager.Delete(ctx, tokenID); err != nil && log != nil {
log.Warn("GitLab legacy token cleanup failed")
}
}
if hostStore != nil {
_ = hostStore.SetHost(ctx, "")
}
}
func findLegacyPAT(ctx context.Context, secrets SecretProvider) (string, string, error) {
if secrets == nil {
return "", "", nil
}
items, err := secrets.List(ctx)
if err != nil {
return "", "", err
}
for _, item := range items {
if item.HasValue && (item.Name == secretNameToken || item.Name == secretNameTokenLower) {
value, err := secrets.Reveal(ctx, item.ID)
return value, item.ID, err
}
}
return "", "", nil
}