Thank you for reading this post, don't forget to subscribe!
появилась задача перетащить гитлаб из облака в self-hosted - миграциями со стороны гитлаба переменные не перетаскиваются, поэтому воспользуемся следующим методом:
на уровне группы создаём токен
так же токен на уровне группы создаём и в нашем self-hosted gitlab
чтобы скачать все переменные воспользуемся следующим запросом:
1 2 |
curl --header "PRIVATE-TOKEN: glpat-BYpCKcoE2P8PKn" "https://gitlab.com/api/v4/groups/614629/variables" > group_1.json |
тут
glpat-BYpCKcoE2P8PKn - это наш токен
614629 - это id группы
group_1.json - это файл куда мы сохраняем переменные
для загрузки в наш self-hosted гитлаб воспользуемся скриптом upload.sh
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#!/bin/bash # Задайте URL вашего self-hosted GitLab и ID группы (можно получить через API или из интерфейса) GITLAB_URL="https://gitlab.test.com" GROUP_ID="7" #ВАШ_GROUP_ID # Ваш персональный токен с нужными правами PRIVATE_TOKEN="glpat-XjLkRqQZKp89xS" # Имя файла в котором находятся скачанные переменные VARIABLE_FILE_NAME="group_2.json" # Перебираем каждую переменную в файле VARIABLE_FILE_NAME for row in $(jq -c '.[]' $VARIABLE_FILE_NAME); do key=$(echo $row | jq -r '.key') value=$(echo $row | jq -r '.value') protected=$(echo $row | jq -r '.protected') masked=$(echo $row | jq -r '.masked') environment_scope=$(echo $row | jq -r '.environment_scope') # Отправляем запрос на создание переменной в целевой группе curl --request POST --header "PRIVATE-TOKEN: $PRIVATE_TOKEN" \ "$GITLAB_URL/api/v4/groups/$GROUP_ID/variables" \ --form "key=$key" \ --form "value=$value" \ --form "protected=$protected" \ --form "masked=$masked" \ --form "environment_scope=$environment_scope" echo "" echo "Переменная $key импортирована" echo "_____________________________" done |
в этом скрипте нужно заполнить переменные:
GITLAB_URL - адрес вашего self-hosted gitlab
GROUP_ID - id группы вашего проекта
PRIVATE_TOKEN - токен который вы создавали на всю группу
VARIABLE_FILE_NAME - файл в котором сохранены все переменные.
дальше можно запускать загрузку
bash upload.sh
всё.
альтернативный вариант
перетаскивает и групповые переменные и переменные в проектах
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 |
#!/bin/bash set -e # === Настройки исходного (облачного) GitLab === GITLAB_CLOUD_TOKEN="glpat-Rn5Tc-AexVubiVn9q" GITLAB_CLOUD_DOMAIN="gitlab.com" GITLAB_CLOUD_GROUP="tech-tech" # === Настройки целевого (self-hosted) GitLab === GITLAB_SELF_HOSTED_TOKEN="glpat-R__zxP6QxLJMzyzx" GITLAB_SELF_HOSTED_DOMAIN="gitlab.infra.tech.tech" GITLAB_SELF_HOSTED_GROUP="tech-tech" # Функция логирования log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" } # Функция для URL‑кодирования (исправленная версия) urlencode() { local string="${1}" local strlen=${#string} local encoded="" local pos c o for (( pos=0; pos<strlen; pos++ )); do c=${string:$pos:1} case "$c" in [-_.~a-zA-Z0-9]) o="$c" ;; *) printf -v o '%%%02X' "'$c" ;; esac encoded+="$o" done echo "$encoded" } ############################################################################### # Функция process_group: рекурсивно обрабатывает группу, её групповые переменные, # переменные проектов, а затем обходит подгруппы. ############################################################################### process_group() { local source_group_path="$1" local target_group_path="$2" # Получаем информацию об исходной группе local source_group_json source_group_json=$(curl -s --header "PRIVATE-TOKEN: $GITLAB_CLOUD_TOKEN" \ "https://$GITLAB_CLOUD_DOMAIN/api/v4/groups/$(urlencode "$source_group_path")") local source_group_id source_group_id=$(echo "$source_group_json" | jq -r '.id') if [ -z "$source_group_id" ] || [ "$source_group_id" = "null" ]; then log "ОШИБКА: Группа-источник '$source_group_path' не найдена." return fi # Получаем информацию о целевой группе local target_group_json target_group_json=$(curl -s --header "PRIVATE-TOKEN: $GITLAB_SELF_HOSTED_TOKEN" \ "https://$GITLAB_SELF_HOSTED_DOMAIN/api/v4/groups/$(urlencode "$target_group_path")") local target_group_id target_group_id=$(echo "$target_group_json" | jq -r '.id') if [ -z "$target_group_id" ] || [ "$target_group_id" = "null" ]; then log "ОШИБКА: Целевая группа '$target_group_path' не найдена." return fi log "=== Обработка группы '$source_group_path' (ID: $source_group_id, целевая ID: $target_group_id) ===" # Перенос групповых переменных local group_vars_json group_vars_json=$(curl -s --header "PRIVATE-TOKEN: $GITLAB_CLOUD_TOKEN" \ "https://$GITLAB_CLOUD_DOMAIN/api/v4/groups/$source_group_id/variables?per_page=100") local var_count var_count=$(echo "$group_vars_json" | jq '. | length') log "Найдено $var_count переменных в группе '$source_group_path'." echo "$group_vars_json" | jq -c '.[]' | while read -r var; do local key value protected masked environment_scope variable_type description source_raw raw_val payload key=$(echo "$var" | jq -r '.key') value=$(echo "$var" | jq -r '.value') protected=$(echo "$var" | jq '.protected // false') masked=$(echo "$var" | jq '.masked // false') environment_scope=$(echo "$var" | jq -r '.environment_scope // ""') variable_type=$(echo "$var" | jq -r '.variable_type') description=$(echo "$var" | jq -r '.description // ""') # Извлекаем raw как JSON-литерал (true или false) source_raw=$(echo "$var" | jq '.raw // false') raw_val=$source_raw # Формируем payload (без поля raw) local base_payload base_payload=$(jq -n \ --arg key "$key" \ --arg value "$value" \ --argjson protected "$protected" \ --argjson masked "$masked" \ --arg environment_scope "$environment_scope" \ --arg variable_type "$variable_type" \ --arg description "$description" \ '{key: $key, value: $value, protected: $protected, masked: $masked, environment_scope: $environment_scope, variable_type: $variable_type, description: $description}') payload=$(echo "$base_payload" | jq --argjson raw "$raw_val" '. + {raw: $raw}') # Перенос переменной в целевую группу local existing existing=$(curl -s --header "PRIVATE-TOKEN: $GITLAB_SELF_HOSTED_TOKEN" \ "https://$GITLAB_SELF_HOSTED_DOMAIN/api/v4/groups/$(urlencode "$target_group_path")/variables/$(urlencode "$key")") if echo "$existing" | jq -e '.id' > /dev/null 2>&1; then local target_raw target_raw=$(echo "$existing" | jq '.raw') if [ "$target_raw" != "$raw_val" ]; then log "Группа: Переменная '$key': отличается raw (целевой: $target_raw, желаемый: $raw_val). Удаляю и создаю заново." curl -s --request DELETE --header "PRIVATE-TOKEN: $GITLAB_SELF_HOSTED_TOKEN" \ "https://$GITLAB_SELF_HOSTED_DOMAIN/api/v4/groups/$(urlencode "$target_group_path")/variables/$(urlencode "$key")" > /dev/null log "Создаю переменную '$key' в группе '$target_group_path'…" curl -s --request POST --header "Content-Type: application/json" --header "PRIVATE-TOKEN: $GITLAB_SELF_HOSTED_TOKEN" \ -d "$payload" "https://$GITLAB_SELF_HOSTED_DOMAIN/api/v4/groups/$(urlencode "$target_group_path")/variables" > /dev/null else log "Обновляю переменную '$key' в группе '$target_group_path'…" curl -s --request PUT --header "Content-Type: application/json" --header "PRIVATE-TOKEN: $GITLAB_SELF_HOSTED_TOKEN" \ -d "$payload" "https://$GITLAB_SELF_HOSTED_DOMAIN/api/v4/groups/$(urlencode "$target_group_path")/variables/$(urlencode "$key")" > /dev/null fi else log "Создаю переменную '$key' в группе '$target_group_path'…" curl -s --request POST --header "Content-Type: application/json" --header "PRIVATE-TOKEN: $GITLAB_SELF_HOSTED_TOKEN" \ -d "$payload" "https://$GITLAB_SELF_HOSTED_DOMAIN/api/v4/groups/$(urlencode "$target_group_path")/variables" > /dev/null fi done # Перенос переменных для проектов внутри данной группы (с пагинацией через временный файл) local projects_json_all_file projects_json_all_file=$(mktemp) echo '[]' > "$projects_json_all_file" local page=1 while : ; do log "Загрузка страницы проектов для группы '$source_group_path', страница $page" local projects_page_json projects_page_json=$(curl -s --header "PRIVATE-TOKEN: $GITLAB_CLOUD_TOKEN" \ "https://$GITLAB_CLOUD_DOMAIN/api/v4/groups/$(urlencode "$source_group_path")/projects?include_subgroups=false&per_page=100&page=$page") if [[ "$projects_page_json" == "[]" ]]; then break fi local tmp_file tmp_file=$(mktemp) jq -s '.[0] + .[1]' "$projects_json_all_file" <(echo "$projects_page_json") > "$tmp_file" mv "$tmp_file" "$projects_json_all_file" ((page++)) done jq -c '.[]' "$projects_json_all_file" | while read -r proj; do local source_project_id path_with_namespace source_project_id=$(echo "$proj" | jq -r '.id') path_with_namespace=$(echo "$proj" | jq -r '.path_with_namespace') log "Проект: $path_with_namespace (ID: $source_project_id)" local project_vars project_vars=$(curl -s --header "PRIVATE-TOKEN: $GITLAB_CLOUD_TOKEN" \ "https://$GITLAB_CLOUD_DOMAIN/api/v4/projects/$source_project_id/variables") local var_count var_count=$(echo "$project_vars" | jq '. | length') log "Найдено $var_count переменных в проекте '$path_with_namespace'." local relative_path relative_path=$(echo "$path_with_namespace" | sed "s#^$GITLAB_CLOUD_GROUP/##") local target_project_full_path="$GITLAB_SELF_HOSTED_GROUP/$relative_path" local encoded_target_project encoded_target_project=$(echo "$target_project_full_path" | sed 's/\//%2F/g') local target_project_json target_project_json=$(curl -s --header "PRIVATE-TOKEN: $GITLAB_SELF_HOSTED_TOKEN" \ "https://$GITLAB_SELF_HOSTED_DOMAIN/api/v4/projects/$encoded_target_project") local target_project_id target_project_id=$(echo "$target_project_json" | jq -r '.id') if [ -z "$target_project_id" ] || [ "$target_project_id" = "null" ]; then log "Целевой проект '$target_project_full_path' не найден. Пропускаю." log "--------------------------------------------" continue fi log "Целевой проект: $target_project_full_path (ID: $target_project_id)" echo "$project_vars" | jq -c '.[]' | while read -r var; do local key value protected masked environment_scope variable_type description source_raw payload key=$(echo "$var" | jq -r '.key') value=$(echo "$var" | jq -r '.value') protected=$(echo "$var" | jq -r '.protected') masked=$(echo "$var" | jq -r '.masked') environment_scope=$(echo "$var" | jq -r '.environment_scope') variable_type=$(echo "$var" | jq -r '.variable_type') description=$(echo "$var" | jq -r '.description // ""') source_raw=$(echo "$var" | jq -r '.raw // "false"') if [ "$source_raw" = "true" ]; then raw_val=true else raw_val=false fi local base_payload base_payload=$(jq -n \ --arg key "$key" \ --arg value "$value" \ --argjson protected "$protected" \ --argjson masked "$masked" \ --arg environment_scope "$environment_scope" \ --arg variable_type "$variable_type" \ --arg description "$description" \ '{key: $key, value: $value, protected: $protected, masked: $masked, environment_scope: $environment_scope, variable_type: $variable_type, description: $description}') payload=$(echo "$base_payload" | jq --argjson raw "$raw_val" '. + {raw: $raw}') local existing existing=$(curl -s --header "PRIVATE-TOKEN: $GITLAB_SELF_HOSTED_TOKEN" \ "https://$GITLAB_SELF_HOSTED_DOMAIN/api/v4/projects/$target_project_id/variables/$(urlencode "$key")") if echo "$existing" | jq -e '.key' > /dev/null 2>&1; then local target_raw target_raw=$(echo "$existing" | jq -r '.raw') if [ "$target_raw" != "$raw_val" ]; then log "Проект '$target_project_full_path': Переменная '$key': raw отличается (целевой: $target_raw, желаемый: $raw_val). Удаляю и создаю заново." curl -s --request DELETE --header "PRIVATE-TOKEN: $GITLAB_SELF_HOSTED_TOKEN" \ "https://$GITLAB_SELF_HOSTED_DOMAIN/api/v4/projects/$target_project_id/variables/$(urlencode "$key")" > /dev/null log "Создаю переменную '$key' в проекте '$target_project_full_path'…" curl -s --request POST --header "Content-Type: application/json" --header "PRIVATE-TOKEN: $GITLAB_SELF_HOSTED_TOKEN" \ -d "$payload" "https://$GITLAB_SELF_HOSTED_DOMAIN/api/v4/projects/$target_project_id/variables" > /dev/null else log "Обновляю переменную '$key' в проекте '$target_project_full_path'…" curl -s --request PUT --header "Content-Type: application/json" --header "PRIVATE-TOKEN: $GITLAB_SELF_HOSTED_TOKEN" \ -d "$payload" "https://$GITLAB_SELF_HOSTED_DOMAIN/api/v4/projects/$target_project_id/variables/$(urlencode "$key")" > /dev/null fi else log "Создаю переменную '$key' в проекте '$target_project_full_path'…" curl -s --request POST --header "Content-Type: application/json" --header "PRIVATE-TOKEN: $GITLAB_SELF_HOSTED_TOKEN" \ -d "$payload" "https://$GITLAB_SELF_HOSTED_DOMAIN/api/v4/projects/$target_project_id/variables" > /dev/null fi done log "--------------------------------------------" done rm "$projects_json_all_file" # Рекурсивно обходим подгруппы текущей группы local subgroups_json subgroups_json=$(curl -s --header "PRIVATE-TOKEN: $GITLAB_CLOUD_TOKEN" \ "https://$GITLAB_CLOUD_DOMAIN/api/v4/groups/$(urlencode "$source_group_path")/subgroups?per_page=100") local sub_count sub_count=$(echo "$subgroups_json" | jq '. | length') if [ "$sub_count" -gt 0 ]; then local i sub_path sub_id for (( i=0; i<sub_count; i++ )); do sub_path=$(echo "$subgroups_json" | jq -r ".[$i].full_path") sub_id=$(echo "$subgroups_json" | jq -r ".[$i].id") log "----> Рекурсивная обработка подгруппы '$sub_path' (ID: $sub_id) ---->" process_group "$sub_path" "$sub_path" done fi } # Запускаем обработку, начиная с основной группы process_group "$GITLAB_CLOUD_GROUP" "$GITLAB_SELF_HOSTED_GROUP" log "Перенос переменных завершён." |
нам нужно создать токены как в облачном так и в self hosted gitlab
и указать группу (подгруппы будут рекурсивно обрабатываться)
# === Настройки исходного (облачного) GitLab === GITLAB_CLOUD_TOKEN="glpat-BoGxdtMTE2P8PKn"
GITLAB_CLOUD_DOMAIN="gitlab.com" # Путь к основной группе-источнику (без URL‑кодирования)
GITLAB_CLOUD_GROUP="tech-tech"
# === Настройки целевого (self‑hosted) GitLab ===
GITLAB_SELF_HOSTED_TOKEN="glpat-gxqqBY3ArKf-z91A8FZB"
GITLAB_SELF_HOSTED_DOMAIN="gitlab.infra.tech.tech" # Путь к основной группе-назначению (структура подгрупп совпадает)
GITLAB_SELF_HOSTED_GROUP="tech-tech"