49177d742f
Co-authored-by: ayattara <alassane.yattara@savoirfairelinux.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
185 lines
5.5 KiB
Go
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
|
|
}
|