- PVSM.RU - https://www.pvsm.ru -
Когда проектов в GitLab становится много, довольно быстро появляется одна и та же задача: найти, где используется конкретный API, URL, env-переменная или конфигурационный параметр.
Пока репозиториев мало, всё просто: открыл поиск, ввел строку, получил результат. Но когда проектов уже больше сотни, а нужные вхождения лежат не только в коде, но и в YAML-конфигах, Helm-чартах, .env и JSON-файлах, жизнь становится менее романтичной.
Первый лобовой вариант — просто скачать все проекты локально и искать по ним через grep, ripgrep или IDE. Работает, но тащить 100+ репозиториев на локальную машину ради одной проверки — идея так себе. Ноутбук, скорее всего, энтузиазма не разделит.
Мне хотелось искать прямо поверх GitLab, без локального зеркала всей группы репозиториев. Я начал с просмотра готовых вариантов, а в итоге пришёл к своему гибридному краулеру: код ищется через GitLab API, а конфиги добираются отдельным глубоким обходом файлов. В результате поиск по 100+ проектам сократился с часов до нескольких минут.
Коротко: я не строил “универсальную платформу поиска по коду”. Я решал вполне прикладную инженерную задачу: быстро проверить большую группу проектов и не пропустить то, что лежит в конфигах.
Чтобы не перегружать статью полным листингом, я вынес полную версию скрипта в отдельный репозиторий на GitHub [1]. В статье оставил только ключевые фрагменты: логику API Search, Deep Search и параллельную обработку.
Перед тем как писать своё решение, я прошёлся по вариантам, которые напрашиваются первыми.
|
Подход |
Плюсы |
Минусы |
Почему не подошёл |
|---|---|---|---|
|
GitLab UI Search |
Быстро, просто, ничего не нужно настраивать |
Неудобно на 100+ проектах, неполно по конфигам |
Для массовой проверки слишком ручной |
|
Локальный поиск по клонам ( |
Полный контроль, точный поиск |
Нужно клонировать и обновлять все репозитории |
Слишком тяжело для разовой или периодической задачи |
|
GitLab API Search |
Быстро, работает прямо поверх GitLab |
Те же ограничения индексированного поиска |
Хорошо для кода, но не для всех конфигов |
|
Sourcegraph и похожие инструменты |
Очень быстро, удобно, мощно |
Это уже отдельный инструмент и отдельное внедрение |
Слишком тяжёлое решение под мою задачу |
|
Свой гибридный краулер |
Гибко, без отдельной инфраструктуры, можно заточить под задачу |
Нужно написать и поддерживать |
Оказался лучшим компромиссом |
Я не пытался заменить Sourcegraph или построить корпоративный поиск по коду. Задача была заметно проще: быстро и надёжно искать по большой группе GitLab-проектов без лишней инфраструктуры.
Очень быстро стало понятно, что разные типы файлов ведут себя по-разному.
|
Тип файлов |
Что лучше работает |
Почему |
|---|---|---|
|
Код ( |
API Search |
GitLab нормально индексирует код |
|
Документация ( |
API Search |
Быстро и обычно этого достаточно |
|
Конфиги ( |
Deep Search |
Здесь встроенный поиск чаще промахивается |
|
Бинарные файлы |
Не искать |
Это просто лишняя трата времени |
Отсюда и родилась основная идея: не искать всё одним способом.
Там, где GitLab уже умеет искать быстро, лучше использовать его сильную сторону. Там, где он периодически промахивается, нужно добирать результат вручную.
Мой скрипт работает по довольно простой логике:
Получает список проектов из GitLab-группы.
Отфильтровывает архивные и неактуальные.
Для каждого проекта запускает:
быстрый API Search по коду;
глубокий обход конфигов.
Объединяет результаты.
Формирует отчёты.
Получается такая схема:
GitLab Group
│
├── Получить список проектов
│
├── Отфильтровать архивные / неактуальные
│
├── Для каждого проекта:
│ ├── API Search → код, docs, scripts
│ └── Deep Search → yaml, json, env
│
├── Объединить результаты
│
└── Сформировать:
├── Детальный отчёт
├── Суммарный отчёт
└── Файл ошибок
В каком-то смысле всё решение строится на одной простой мысли: не делать вид, что один инструмент одинаково хорош во всём.
group = gl.groups.get(GROUP_ID)
all_projects = group.projects.list(include_subgroups=True, all=True)
Дальше отбрасываю архивные и неактуальные репозитории, чтобы не тратить на них время.
На практике это важно. Если без фильтрации пройтись по всему подряд, время растёт быстрее пользы.
def api_search(gl, project_id, search_terms):
results = {term: [] for term in search_terms}
for term in search_terms:
blobs = gl.search('blobs', term, project_id=project_id)
for blob in blobs:
file_path = blob.get('path', '')
file_ext = os.path.splitext(file_path)[1].lower()
if file_ext in CODE_EXTENSIONS or file_ext == '':
results[term].append({
'path': file_path,
'url': blob.get('web_url', '#'),
'found_by': 'API'
})
return results
Именно API Search даёт основную скорость на кодовых файлах и документации.
def deep_search_configs(gl, project_id, search_terms):
results = {term: [] for term in search_terms}
project = gl.projects.get(project_id)
files = project.repository_tree(recursive=True, ref='master')
for file in files:
if file['type'] != 'blob':
continue
ext = os.path.splitext(file['name'])[1].lower()
if ext not in CONFIG_EXTENSIONS:
continue
content = project.files.get(file_path=file['path'], ref='master').decode()
content_lower = content.lower()
for term in search_terms:
if term.lower() in content_lower:
results[term].append({
'path': file['path'],
'found_by': 'Deep'
})
return results
Да, это медленнее. Но именно здесь добираются YAML, .env и другие конфиги, которые встроенный поиск может пропустить.
В текущей версии скрипта поиск идёт только по master. Для моей задачи этого было достаточно: внутри компании проверять нужно было именно основную ветку, потому что актуальные конфиги и рабочие настройки лежали там.
Поиск по всем веткам можно добавить позже. Но для первой версии я не хотел усложнять логику и увеличивать время выполнения без необходимости.
Это как раз тот случай, когда сначала лучше закрыть рабочую задачу, а уже потом делать решение более универсальным.
Если обрабатывать 100+ проектов последовательно, скрипт становится заметно медленнее. Поэтому я добавил ThreadPoolExecutor:
with ThreadPoolExecutor(max_workers=3) as executor:
futures = {executor.submit(process_one_project, gl, p): p for p in active_projects}
for future in as_completed(futures):
result = future.result()
results_list.append(result)
Это дало хороший прирост по времени без лишнего усложнения.
Без параллельности скрипт тоже работал, но уже без особого энтузиазма. С параллельностью он стал ощущаться как нормальный рабочий инструмент, а не как процесс, который лучше запустить и уйти пить чай.
Скрипт формирует два основных отчёта.
Он показывает:
проект;
путь к файлу;
найденный термин;
строку;
способ поиска;
ссылку на GitLab.
Пример:
ПРОЕКТ: service-a
Путь: backend/service-a
'SERVICE_EXAMPLE': 2 вхождения
- deploy/prod/values.yml:42 [Deep]
Строка 42: SERVICE_EXAMPLE: "{{ .Values.secrets.apiKey }}"
https://gitlab.example.com/backend/service-a/-/blob/master/deploy/prod/values.yml#L42
Он показывает:
сколько проектов затронуто;
сколько всего вхождений;
сколько найдено через API;
сколько найдено через Deep Search.
Такой формат оказался удобен не только для самой проверки, но и для дальнейшей работы: результат можно быстро копировать в задачу, тикет или внутреннюю документацию.
Здесь не было одной “волшебной кнопки”. Сработала комбинация из нескольких решений.
|
Что сделал |
Что это дало |
|---|---|
|
Не сканировал все файлы подряд |
Убрал самый тяжёлый сценарий |
|
Код оставил на API Search |
Быстрый поиск там, где он и так работает |
|
Конфиги вынес в Deep Search |
Добрал пропущенные совпадения |
|
Добавил параллельность |
Сократил общее время обработки |
|
Отсёк архивные проекты |
Не тратил время впустую |
Если совсем коротко: я не сделал поиск “умнее”, я сделал его более прагматичным. И этого уже хватило.
Чтобы статья не выглядела как “я написал идеальный инструмент”, лучше честно проговорить ограничения.
|
Ограничение |
Что это значит |
|---|---|
|
Поиск только по |
Пока не ищу по всем веткам |
|
Deep Search ограничен по типам файлов |
Это сделано ради скорости |
|
Нет regex |
Ищу точные вхождения |
|
Нет дедупликации API/Deep |
Одинаковые находки можно улучшить позже |
|
Нет отдельного UI |
Сейчас это консольный инструмент |
Такой вариант хорошо работает, когда нужно быстро проверить большую группу репозиториев по конкретным терминам.
Например:
найти использование API-ручки перед изменением контракта;
проверить, где ещё осталась env-переменная;
собрать список сервисов, где используется старый URL;
понять, в каких конфигах ещё не обновлён параметр;
быстро оценить влияние изменения какого-то внешнего сервиса.
Особенно хорошо такой подход заходит в сценариях “нужно проверить сейчас”, а не “давайте строить отдельную поисковую платформу на квартал”.
Следующая версия напрашивается сама собой:
определять default branch автоматически;
добавить поиск по нескольким веткам;
поддержать regex;
дедуплицировать результаты;
добавить CSV / JSON-экспорт;
аккуратнее обработать rate limits и retries;
сделать небольшой CLI или web-интерфейс.
То есть потенциал роста есть. Но даже текущая версия уже решает ту задачу, ради которой всё затевалось.
Встроенный поиск GitLab оказался хорош не везде одинаково: по коду — нормально, по конфигам — уже не так надёжно. Поэтому я не стал пытаться искать всё одним способом и сделал гибридный подход:
код — через API Search;
конфиги — через прямой обход файлов;
результаты — в один отчёт;
обработку — в несколько потоков.
В итоге поиск по 100+ GitLab-проектам перестал быть многочасовой рутиной и стал обычной технической операцией на несколько минут.
Главный вывод здесь довольно простой: не всегда нужно строить большой универсальный инструмент. Иногда достаточно честно разложить задачу на части и для каждой выбрать самый практичный способ.
Если у вас были похожие сценарии с GitLab-группами, конфигами или поиском по большой кодовой базе, интересно было бы сравнить подходы. В таких задачах чужие инженерные костыли обычно не менее полезны, чем свои.
Автор: HaperStrelkov
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/poisk/448799
Ссылки в тексте:
[1] GitHub: https://github.com/Panib/crauler_version_1/blob/main/crauler.py
[2] Источник: https://habr.com/ru/articles/1019332/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1019332
Нажмите здесь для печати.