diff --git a/README.md b/README.md index 4bc1bcc..a471927 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,120 @@ OneScript Package Manager Справка из википедии: [Система управления пакетами](https://ru.wikipedia.org/wiki/%D0%A1%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B0_%D1%83%D0%BF%D1%80%D0%B0%D0%B2%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F_%D0%BF%D0%B0%D0%BA%D0%B5%D1%82%D0%B0%D0%BC%D0%B8) +# Адрес хаба прямо в команде + +Установка принимает адрес пакета в хабе вместо имени либо хаб опцией `--hub` и пул опцией `--pool` +(переменные окружения `OPM_HUB` и `OPM_POOL`), публикация — только опциями; без `--hub` — хаб по умолчанию, +без `--pool` — пул хаба по умолчанию; запись в `opm.cfg` не нужна: + +```sh +opm install localhost:3380/corp/my-package@stable # хост, порт, пул, пакет, тег +opm install hub.example.com/asserts # пакет в пуле хаба по умолчанию +opm install https://hub.example.com/public/asserts # схема указана явно +opm install --hub localhost:3380 --pool corp my-package@stable +opm install --hub localhost:3380 --all-pools my-package@stable # версию выбирает хаб по всем доступным пулам +opm push --hub localhost:3380 --pool corp my-package-1.0.0.ospx +opm push --hub https://hub.example.com --token <токен> my-package-1.0.0.ospx +``` + +Адрес пакета: `[схема://]хост[:порт]/[пул/]пакет[@спецификация]`; в `--hub` — `[схема://]хост[:порт]` без пути. + +* В адресе пакета последний сегмент пути — пакет, сегмент перед ним — пул; без пула — пул хаба по умолчанию. + Зависимости пакета ставятся с того же сервера, у `install --hub` — с хаба из опций. +* `install --all-pools` просит хаб выбрать версию по всем пулам, открытым токену (без токена — по публичным): + адреса `/api/v1/download/<пакет>/<файл>` с теми же `spec` и `engine`; с `--pool` не сочетается. +* Отказ: путь в `--hub`, адрес пакета вместе с `--hub`, `install --hub` вместе с `-a`, `-f`, `-u`, `-m`. +* Строка считается адресом, если в ней есть схема либо путь после хоста с портом, с точкой в имени + или `localhost`. Остальное — имя пакета. +* Без схемы берётся `http` для `localhost`, `127.*` и адреса с явным портом, иначе `https`. +* Авторизация берётся у сервера из `opm.cfg` с тем же адресом и портом. +* `opm push --hub` отправляет токен из опции `--token` (переменная окружения `GITHUB_OAUTH_TOKEN`) + или из настройки `ТокенПубликацииПакетов`. Канал при публикации в такой хаб необязателен. +* Опция `-m|--mirror` у `push` устарела и печатает предупреждение: вместо неё — `--hub`. +* `opm push --force` просит хаб опубликовать поверх уже занятого номера версии (параметр `force=1` + в запросе публикации); перезапишет ли он версию, решает хаб. + +# Версия пакета после @ + +`install`, `update` и `download` принимают после `@` точную версию, имя тега или выражение версии: + +```sh +opm install vrunner@1.0.0 # точная версия +opm install vrunner@stable # версия под тегом +opm install vrunner@^1.4 # старшая версия, подходящая под выражение +opm install vrunner # версию выбирает хаб +``` + +Спецификация уходит хабу параметром `?spec=`, имя файла в адресе прежнее — `<пакет>-<спецификация>.ospx`. +Тег и выражение версии разрешает только хаб, принимающий `spec`; хаб без него отвечает 404, и opm так и говорит. +Точная версия ставится с любым хабом. Файловый сервер (`file://`) отдаёт только файл названной версии +и не подставляет вместо неё старшую. + +К запросу файла пакета opm добавляет параметр `engine` — версию установленного OneScript. Хаб, который его понимает, +выбирает версию под этот движок там, где версию выбирает он (без `@`, тег, выражение); точная версия отдаётся как есть. +Если хаб отказал и объяснил причину в ответе, opm показывает её. + +# Ошибки связи + +Если хаб ответил, но файла не отдал, причина называется по коду ответа: 404 — файл не найден, +401 и 403 — отказ в доступе, иначе — код ответа и имя файла. + +```console +$ opm install my-package +Ошибка установки пакета my-package: файл my-package.ospx в хабе не найден +``` + +Если ответа нет, opm называет адрес сервера: `Не удалось подключиться к http://hub.example.com:3380`. +Исходное описание ошибки уходит в отладочный журнал `oscript.app.opm`. При переборе нескольких +серверов из `opm.cfg` недоступный сервер остаётся предупреждением, и поиск идёт дальше. + +# Проверка контрольной суммы + +Хаб присылает sha256 файла пакета в заголовке `X-Checksum-Sha256`. opm сверяет с ним скачанный `.ospx` +и при расхождении отказывает в установке, не записывая файл. Ответ без заголовка (хаб без его поддержки, +файловый сервер) не сверяется. + +# Доверенная публикация из CI + +Конвейер GitHub Actions или GitLab CI публикует пакет без токена: `opm push --trusted` берёт у конвейера +OIDC id-token и отправляет его хабу вместо токена, а хаб сверяет его с записью доверия пакета. +Запись доверия (провайдер, репозиторий, при необходимости конвейер и ref) заводится на странице пакета в хабе. + +GitHub Actions выдаёт id-token конвейеру с разрешением `id-token: write`: + +```yaml +permissions: + contents: read + id-token: write +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: otymko/setup-onescript@v1.5.1 + - run: opm install -l && opm build . + - run: opm push --hub https://hub.example.com/corp --trusted my-package-1.0.0.ospx +``` + +GitLab CI кладёт id-token в переменную блока `id_tokens`; opm читает её под именем `OPENHUB_TOKEN`: + +```yaml +publish: + id_tokens: + OPENHUB_TOKEN: + aud: https://hub.example.com + script: + - opm install -l && opm build . + - opm push --hub https://hub.example.com/corp --trusted my-package-1.0.0.ospx +``` + +* В GitHub Actions id-token запрашивается с аудиторией, равной адресу хаба (`https://hub.example.com`); + другую аудиторию задаёт `--trusted-audience`. В GitLab CI аудиторию задаёт `aud` в блоке `id_tokens`. +* С `--trusted` токен из `--token` и настройки `ТокенПубликацииПакетов` не отправляется. +* Конвейер не выдал id-token — opm отказывает и называет, чего не хватает: `permissions: id-token: write` + в GitHub Actions, переменной `OPENHUB_TOKEN` в блоке `id_tokens` в GitLab CI. +* Подпись id-token и запись доверия проверяет хаб; отказ хаба opm показывает его сообщением. + # Сервера В настоящее время пакеты хранятся на двух серверах: - [hub.oscript.io](http://hub.oscript.io/download) - основной хаб пакетов diff --git a/features/install-hub.feature b/features/install-hub.feature new file mode 100644 index 0000000..a08bf8f --- /dev/null +++ b/features/install-hub.feature @@ -0,0 +1,32 @@ +# language: ru + +Функциональность: Установка с хаба из опций + +Как разработчик + Я хочу ставить пакеты с хаба, названного опцией --hub + Чтобы не путать адрес хаба в опции с адресом пакета в аргументе + +Контекст: Тестовый контекст + Допустим Я очищаю параметры команды "opm" в контексте + И Я устанавливаю путь выполнения команды "opm" к текущей библиотеке + +Сценарий: Адрес пакета вместе с опцией --hub + Допустим Я добавляю параметр "install" для команды "opm" + И Я добавляю параметр "--hub" для команды "opm" + И Я добавляю параметр "hub.example" для команды "opm" + И Я добавляю параметр "hub.example/corp/pkg" для команды "opm" + Когда Я выполняю команду "opm" + Тогда Вывод команды "opm" содержит "задан адресом вместе с опцией <--hub>: укажите одно из двух" + И Код возврата команды "opm" равен 1 + +Сценарий: Все пулы вместе с одним пулом + Допустим Я добавляю параметр "install" для команды "opm" + И Я добавляю параметр "--hub" для команды "opm" + И Я добавляю параметр "hub.example" для команды "opm" + И Я добавляю параметр "--pool" для команды "opm" + И Я добавляю параметр "corp" для команды "opm" + И Я добавляю параметр "--all-pools" для команды "opm" + И Я добавляю параметр "pkg" для команды "opm" + Когда Я выполняю команду "opm" + Тогда Вывод команды "opm" содержит "Опция <--all-pools> не сочетается с опцией <--pool>" + И Код возврата команды "opm" равен 1 diff --git a/packagedef b/packagedef index f39977b..30f3a55 100644 --- a/packagedef +++ b/packagedef @@ -47,6 +47,7 @@ .ОпределяетКласс("СерверПакетов", "src/core/Классы/СерверПакетов.os") .ОпределяетКласс("СериализацияМетаданныхПакета", "src/core/Классы/СериализацияМетаданныхПакета.os") .ОпределяетКласс("УстановкаПакета", "src/core/Классы/УстановкаПакета.os") + .ОпределяетМодуль("АдресаХаба", "src/core/Модули/АдресаХаба.os") .ОпределяетМодуль("КонстантыOpm", "src/core/Модули/КонстантыOpm.os") .ОпределяетМодуль("НастройкиOpm", "src/core/Модули/НастройкиOpm.os") .ОпределяетМодуль("РаботаСВерсиями", "src/core/Модули/РаботаСВерсиями.os") @@ -54,5 +55,6 @@ .ОпределяетМодуль("РаботаСПакетами", "src/core/Модули/РаботаСПакетами.os") .ОпределяетМодуль("РежимУстановкиПакетов", "src/core/Модули/РежимУстановкиПакетов.os") .ОпределяетМодуль("СерверыПакетов", "src/core/Модули/СерверыПакетов.os") + .ОпределяетМодуль("ТокенКонвейераCI", "src/core/Модули/ТокенКонвейераCI.os") .ОпределяетМодуль("ОбщегоНазначенияOpm", "src/core/Модули/ОбщегоНазначенияOpm.os") .ИсполняемыйФайл("src/cmd/opm.os"); diff --git "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Download.os" "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Download.os" index 57e0253..9593464 100644 --- "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Download.os" +++ "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Download.os" @@ -21,7 +21,7 @@ ОпцияЗеркала.Перечисление(ДоступноеЗеркало, ДоступноеЗеркало, "Сервер '" + ДоступноеЗеркало + "'"); КонецЦикла; - КомандаПриложения.Аргумент("PACKAGE", "", "Имя пакета в хабе. Чтобы установить конкретную версию, используйте ИмяПакета@ВерсияПакета") + КомандаПриложения.Аргумент("PACKAGE", "", ПараметрыПриложенияOpm.ОписаниеАргументаПакет()) .ТМассивСтрок() .Обязательный(Ложь); diff --git "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Install.os" "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Install.os" index dad2baf..3672688 100644 --- "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Install.os" +++ "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Install.os" @@ -23,7 +23,13 @@ ОпцияЗеркала.Перечисление(ДоступноеЗеркало, ДоступноеЗеркало, "Сервер '" + ДоступноеЗеркало + "'"); КонецЦикла; - КомандаПриложения.Аргумент("PACKAGE", "", "Имя пакета в хабе. Чтобы установить конкретную версию, используйте ИмяПакета@ВерсияПакета") + КомандаПриложения.Опция("hub", "", "Адрес хаба, с которого ставятся пакеты и их зависимости: [схема://]хост[:порт]; без адреса - хаб по умолчанию.") + .ВОкружении("OPM_HUB"); + КомандаПриложения.Опция("pool", "", "Пул хаба; без пула - пул хаба по умолчанию.") + .ВОкружении("OPM_POOL"); + КомандаПриложения.Опция("all-pools", Ложь, "Ставить из всех доступных пулов хаба: версию выбирает хаб среди пулов, открытых токену."); + + КомандаПриложения.Аргумент("PACKAGE", "", ПараметрыПриложенияOpm.ОписаниеАргументаПакет()) .ТМассивСтрок() .Обязательный(Ложь); @@ -44,6 +50,9 @@ НеобходимоУстановитьЗависимости = Не КомандаПриложения.ЗначениеОпции("skip-install-deps"); СоздаватьФайлыЗапуска = НЕ КомандаПриложения.ЗначениеОпции("skip-create-app"); ИмяСервера = КомандаПриложения.ЗначениеОпции("mirror"); + Хаб = КомандаПриложения.ЗначениеОпции("hub"); + Пул = КомандаПриложения.ЗначениеОпции("pool"); + ПоВсемПулам = КомандаПриложения.ЗначениеОпции("all-pools"); РежимУстановки = РежимУстановкиПакетов.Глобально; Если УстановкаВЛокальныйКаталог Тогда @@ -78,6 +87,14 @@ НастройкаУстановки.СоздаватьФайлыЗапуска = СоздаватьФайлыЗапуска; НастройкаУстановки.ИмяСервера = ИмяСервера; + Если ЗначениеЗаполнено(Хаб) Или ЗначениеЗаполнено(Пул) Или ПоВсемПулам Тогда + Если УстановкаВсехПакетов Или ЗначениеЗаполнено(ФайлПакетаУстановки) Или ЗначениеЗаполнено(ИнтернетСсылкаНаПакет) + Или ЗначениеЗаполнено(ИмяСервера) Тогда + ВызватьИсключение "Опция <--hub> не сочетается с опциями <-a>, <-f>, <-u> и <-m>"; + КонецЕсли; + НастройкаУстановки.ИмяСервера = ПодключитьХаб(Хаб, Пул, ПоВсемПулам, МассивПакетовКУстановке); + КонецЕсли; + Если УстановкаВсехПакетов Тогда РаботаСПакетами.УстановитьВсеПакетыИзОблака(РежимУстановки, ЦелевойКаталог, НастройкаУстановки); ИначеЕсли ПустаяСтрока(ФайлПакетаУстановки) И ПустаяСтрока(ИнтернетСсылкаНаПакет) И МассивПакетовКУстановке.Количество() = 0 Тогда @@ -144,6 +161,34 @@ КонецПроцедуры +// Подключает хаб из опций --hub, --pool и --all-pools: пакеты и их зависимости ставятся с него. +// +// Параметры: +// Хаб - Строка - [схема://]хост[:порт]. +// Пул - Строка - пул хаба; пустой - пул хаба по умолчанию. +// ПоВсемПулам - Булево - выдача по всем доступным пулам хаба; с пулом не сочетается. +// ИменаПакетов - Массив - имена пакетов из аргумента PACKAGE. +// +// Возвращаемое значение: +// Строка - имя сервера пула в настройках. +// +Функция ПодключитьХаб(Знач Хаб, Знач Пул, Знач ПоВсемПулам, Знач ИменаПакетов) + + Если ПоВсемПулам И ЗначениеЗаполнено(Пул) Тогда + ВызватьИсключение "Опция <--all-pools> не сочетается с опцией <--pool>: укажите одно из двух"; + КонецЕсли; + + Адрес = АдресаХаба.Адрес(Хаб, Пул); + Для Каждого ИмяПакета Из ИменаПакетов Цикл + Если АдресаХаба.ЭтоАдресХаба(ИмяПакета) Тогда + ВызватьИсключение СтрШаблон("Пакет <%1> задан адресом вместе с опцией <--hub>: укажите одно из двух", ИмяПакета); + КонецЕсли; + КонецЦикла; + + Возврат РаботаСПакетами.ПодключитьСерверХаба(Адрес, ПоВсемПулам); + +КонецФункции + Функция ЭтоФайлПакета(Знач ИмяПакета) Возврат СтрЗаканчиваетсяНа(НРег(ИмяПакета), ".ospx"); diff --git "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" index 0603d2a..5c9a4e1 100644 --- "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" +++ "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Push.os" @@ -10,22 +10,30 @@ Процедура ОписаниеКоманды(Знач КомандаПриложения) Экспорт - КомандаПриложения.Опция("token", "", "Токен авторизации на GitHub.com + КомандаПриложения.Опция("hub", "", "Адрес хаба публикации: [схема://]хост[:порт].") + .ВОкружении("OPM_HUB"); + КомандаПриложения.Опция("pool", "", "Пул хаба; без пула - пул хаба по умолчанию.") + .ВОкружении("OPM_POOL"); + КомандаПриложения.Опция("token", "", "Токен авторизации в хабе пакетов: для OpenHub - персональный токен доступа, + | для хаба без OpenHub - токен GitHub.com | - Токен авторизации создается на странице https://github.com/settings/tokens | - Токен используется только для проверки авторизации на GitHub.com и прав на репозиторий, | выдача дополнительных разрешений в ""scopes"" НЕ требуется.") .ВОкружении("GITHUB_OAUTH_TOKEN"); ; КомандаПриложения.Опция("f file", "", "Маска или имя файла пакета."); + КомандаПриложения.Опция("force", Ложь, "Опубликовать поверх уже занятого номера версии."); + КомандаПриложения.Опция("trusted", Ложь, "Публиковать id-token конвейера GitHub Actions или GitLab CI вместо токена."); + КомандаПриложения.Опция("trusted-audience", "", "Аудитория id-token в GitHub Actions. По умолчанию - адрес хаба."); КомандаПриложения.Опция("c channel", "auto", "Канал публикации.") .ТПеречисление() .Перечисление("auto", "auto", "Автоматическое определение канала. | В случае отправки из ветки master гит-репозитория данный параметр можно опустить - будет использоваться канал ""stable"". - | В любых других случаях его заполнение обязательно.") + | В любых других случаях его заполнение обязательно, кроме публикации в хаб из опции <--hub>.") .Перечисление("stable", "stable", "Канал содержащий стабильные версии пакетов") .Перечисление("dev", "dev" , "Канал содержащий разработческие версии пакетов") .ВОкружении("OPM_HUB_CHANNEL"); - ОпцияЗеркала = КомандаПриложения.Опция("m mirror", "СерверУдаленногоХранилища", "Имя сервера для публикации. + ОпцияЗеркала = КомандаПриложения.Опция("m mirror", "", "УСТАРЕЛА, указывайте адрес хаба опцией <--hub>. Имя сервера для публикации. | Доступные сервера прописываются в конфигурационном файле opm.cfg, параметр 'СервераПакетов'.") .ВОкружении("OPM_HUB_MIRROR") .ТПеречисление(); @@ -46,6 +54,13 @@ МаскаФайлаПакетаСтарая = КомандаПриложения.ЗначениеОпции("file"); ИмяКаналаПубликации = КомандаПриложения.ЗначениеОпции("channel"); ИмяСервераПакетов = КомандаПриложения.ЗначениеОпции("mirror"); + АдресХаба = КомандаПриложения.ЗначениеОпции("hub"); + Пул = КомандаПриложения.ЗначениеОпции("pool"); + Форсировать = КомандаПриложения.ЗначениеОпции("force"); + + Если ЗначениеЗаполнено(ИмяСервераПакетов) Тогда + Лог.Предупреждение("Опция <-m|--mirror> устарела и будет удалена. Используйте опцию <--hub>"); + КонецЕсли; Если ПустаяСтрока(ТокенАвторизации) Тогда НастройкиИзФайла = ПараметрыПриложенияOpm.СобратьНастройкиИзФайлов(); @@ -65,13 +80,20 @@ ФайлПакета = ПолучитьФайлПакета(МаскаФайлаПакета); - Канал = ПолучитьИмяКаналаПубликации(ИмяКаналаПубликации); + Канал = ПолучитьИмяКаналаПубликации(ИмяКаналаПубликации, ПустаяСтрока(АдресХаба) И ПустаяСтрока(Пул)); - ОтправитьПакетВХаб(ТокенАвторизации, ФайлПакета, Канал, ИмяСервераПакетов); + Сервер = СерверыПакетов.ДляПубликации(АдресХаба, ИмяСервераПакетов, Пул, Форсировать); + Если КомандаПриложения.ЗначениеОпции("trusted") Тогда + ТокенАвторизации = ТокенКонвейераCI.Получить(Сервер, КомандаПриложения.ЗначениеОпции("trusted-audience")); + КонецЕсли; + + Лог.Отладка("Публикация на сервер %1", Сервер.ПолучитьИмя()); + Лог.Информация(Сервер.ОтправитьПакет(ФайлПакета, Канал, ТокенАвторизации)); КонецПроцедуры -Функция ПолучитьИмяКаналаПубликации(Знач ИмяКаналаПубликации) +// Хаб из опции --hub ведёт каналы тегами пакета: неопределённый канал уходит ему пустым. +Функция ПолучитьИмяКаналаПубликации(Знач ИмяКаналаПубликации, Знач КаналОбязателен) ГитРепозиторий = Новый ГитРепозиторий(); ГитРепозиторий.УстановитьРабочийКаталог(ТекущийКаталог()); @@ -80,18 +102,14 @@ Если КаналПубликации = "auto" Тогда - Если НЕ ГитРепозиторий.ЭтоРепозиторий() Тогда - ВызватьИсключение "Невозможно автоматически определить канал. " - + "Требуется явное указание в параметре --channel одного из значений: " + ДопустимыеИменаКаналовЗначения(); - КонецЕсли; - - ИмяВетки = ГитРепозиторий.ПолучитьТекущуюВетку(); - Если ИмяВетки <> "master" Тогда - ВызватьИсключение "Невозможно автоматически определить канал. " - + "Требуется явное указание в параметре --channel одного из значений: " + ДопустимыеИменаКаналовЗначения(); + Если ГитРепозиторий.ЭтоРепозиторий() И ГитРепозиторий.ПолучитьТекущуюВетку() = "master" Тогда + Возврат ДопустимыеИменаКаналов.Стабильный; + ИначеЕсли НЕ КаналОбязателен Тогда + Возврат ""; КонецЕсли; - Возврат ДопустимыеИменаКаналов.Стабильный; + ВызватьИсключение "Невозможно автоматически определить канал. " + + "Требуется явное указание в параметре --channel одного из значений: " + ДопустимыеИменаКаналовЗначения(); Иначе @@ -137,74 +155,6 @@ КонецФункции -Процедура ОтправитьПакетВХаб(Знач ТокенАвторизации, Знач ФайлПакета, Знач Канал, Знач ИмяСервераПакетов) - - ДвоичныеДанныеФайла = Новый ДвоичныеДанные(ФайлПакета.ПолноеИмя); - ДвоичныеДанныеФайлаВBase64 = Base64Строка(ДвоичныеДанныеФайла); - - ДоступныеСервераПакетов = НастройкиOpm.ПолучитьНастройки().СервераПакетов; - - // Для настроек по умолчанию - Сервер = КонстантыOpm.СерверУдаленногоХранилища; - Ресурс = КонстантыOpm.РесурсПубликацииПакетов; - Авторизация = Неопределено; - ДополнительныеЗаголовки = Неопределено; - Таймаут = 600; - - Для Каждого НастройкаСервера Из ДоступныеСервераПакетов Цикл - - Если СтрСравнить(НастройкаСервера.Имя, ИмяСервераПакетов) = 0 Тогда - Сервер = НастройкаСервера.Сервер; - Ресурс = НастройкаСервера.РесурсПубликацииПакетов; - Авторизация = НастройкаСервера.Авторизация; - ДополнительныеЗаголовки = НастройкаСервера.ДополнительныеЗаголовки; - Таймаут = НастройкаСервера.Таймаут; - Прервать; - КонецЕсли; - - КонецЦикла; - - Лог.Отладка("Сервер = %1", Сервер); - Лог.Отладка("Ресурс = %1", Ресурс); - - Заголовки = Новый Соответствие(); - Если ЗначениеЗаполнено(Авторизация) Тогда - Заголовки.Вставить("Authorization", ОбщегоНазначенияOpm.ПрименитьПеременныеСреды(Авторизация)); - Иначе - Заголовки.Вставить("OAUTH-TOKEN", ТокенАвторизации); - КонецЕсли; - Заголовки.Вставить("FILE-NAME", ФайлПакета.Имя); - Заголовки.Вставить("CHANNEL", Канал); - Если ЗначениеЗаполнено(ДополнительныеЗаголовки) Тогда - Для Каждого мЗаголовок Из ДополнительныеЗаголовки Цикл - Заголовки.Вставить(мЗаголовок.Ключ, ОбщегоНазначенияOpm.ПрименитьПеременныеСреды(мЗаголовок.Значение)); - КонецЦикла; - КонецЕсли; - - Соединение = Новый HTTPСоединение(Сервер,,,,, Таймаут); - Запрос = Новый HTTPЗапрос(Ресурс, Заголовки); - Запрос.УстановитьТелоИзДвоичныхДанных(ДвоичныеДанныеФайла); - - Ответ = Соединение.ОтправитьДляОбработки(Запрос); - ТелоОтвета = Ответ.ПолучитьТелоКакСтроку(); - - Если Не КодУспешнойОтправки(Ответ.КодСостояния) Тогда - ВызватьИсключение ТелоОтвета; - КонецЕсли; - - Лог.Информация(ТелоОтвета); - -КонецПроцедуры - -Функция КодУспешнойОтправки(Знач КодСостояния) - - Возврат КодСостояния = 200 // OK - Или КодСостояния = 201 // CREATED - Или КодСостояния = 202 // ACCEPTED - ; - -КонецФункции - Функция ДопустимыеИменаКаналовЗначения() ДопустимыеИменаКаналовЗначения = Новый Массив; diff --git "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Update.os" "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Update.os" index 10244f4..b204dc9 100644 --- "a/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Update.os" +++ "b/src/cmd/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\274\320\260\320\275\320\264\320\260Opm_Update.os" @@ -22,7 +22,7 @@ ОпцияЗеркала.Перечисление(ДоступноеЗеркало, ДоступноеЗеркало, "Сервер '" + ДоступноеЗеркало + "'"); КонецЦикла; - КомандаПриложения.Аргумент("PACKAGE", "", "Имя пакета в хабе. Чтобы установить конкретную версию, используйте ИмяПакета@ВерсияПакета") + КомандаПриложения.Аргумент("PACKAGE", "", ПараметрыПриложенияOpm.ОписаниеАргументаПакет()) .ТМассивСтрок() .Обязательный(Ложь); diff --git "a/src/cmd/\320\234\320\276\320\264\321\203\320\273\320\270/\320\237\320\260\321\200\320\260\320\274\320\265\321\202\321\200\321\213\320\237\321\200\320\270\320\273\320\276\320\266\320\265\320\275\320\270\321\217Opm.os" "b/src/cmd/\320\234\320\276\320\264\321\203\320\273\320\270/\320\237\320\260\321\200\320\260\320\274\320\265\321\202\321\200\321\213\320\237\321\200\320\270\320\273\320\276\320\266\320\265\320\275\320\270\321\217Opm.os" index 034828c..fcee45d 100644 --- "a/src/cmd/\320\234\320\276\320\264\321\203\320\273\320\270/\320\237\320\260\321\200\320\260\320\274\320\265\321\202\321\200\321\213\320\237\321\200\320\270\320\273\320\276\320\266\320\265\320\275\320\270\321\217Opm.os" +++ "b/src/cmd/\320\234\320\276\320\264\321\203\320\273\320\270/\320\237\320\260\321\200\320\260\320\274\320\265\321\202\321\200\321\213\320\237\321\200\320\270\320\273\320\276\320\266\320\265\320\275\320\270\321\217Opm.os" @@ -15,6 +15,17 @@ Возврат "oscript.app.opm"; КонецФункции +// Описание аргумента PACKAGE команд install, download и update. +// +// Возвращаемое значение: +// Строка - текст справки. +// +Функция ОписаниеАргументаПакет() Экспорт + Возврат "Имя пакета в хабе. После @ можно указать точную версию (пакет@1.0.0), + | имя тега (пакет@stable) или выражение версии (пакет@^1.4). + | Вместо имени можно указать адрес пакета в хабе: [схема://]хост[:порт]/[пул/]пакет[@спецификация]."; +КонецФункции + Процедура НастроитьOpm() Экспорт НастройкиOpmИзФайлов = СобратьНастройкиИзФайлов(); diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\237\320\276\320\273\321\203\321\207\320\265\320\275\320\270\321\217\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\237\320\276\320\273\321\203\321\207\320\265\320\275\320\270\321\217\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" index 7355e45..b7295ab 100644 --- "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\237\320\276\320\273\321\203\321\207\320\265\320\275\320\270\321\217\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\237\320\276\320\273\321\203\321\207\320\265\320\275\320\270\321\217\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -10,7 +10,7 @@ КонецПроцедуры -Функция ПолучитьПакет(Знач ИмяПакета, Знач ВерсияПакета, ПутьКФайлуПакета = "", ИмяСервера = "") Экспорт +Функция ПолучитьПакет(Знач ИмяПакета, Знач Спецификация, ПутьКФайлуПакета = "", ИмяСервера = "") Экспорт ОбновитьИндексыКешейПакетов(); @@ -23,8 +23,8 @@ ИмяПакета = ОпределитьИмяАрхива(ИмяПакета); - Если ВерсияПакета <> Неопределено Тогда - ФайлПакета = ИмяПакета + "-" + ВерсияПакета + ".ospx"; + Если Спецификация <> Неопределено Тогда + ФайлПакета = ИмяПакета + "-" + Спецификация + ".ospx"; Иначе ФайлПакета = ИмяПакета + ".ospx"; КонецЕсли; @@ -39,15 +39,19 @@ ПереченьСерверов = ИндексКешаПакетов[ИмяПакета]; - Ответ = ЗапроситьПакет(ПереченьСерверов, ИмяСервера, ИмяРесурса); + Полученное = ЗапроситьПакет(ПереченьСерверов, ИмяСервера, ИмяРесурса, Спецификация); + Ответ = Полученное.Ответ; Если Не Ответ = Неопределено Тогда Лог.Отладка("Файл получен"); - Ответ.ПолучитьТелоКакДвоичныеДанные().Записать(ПутьКФайлуПакета); + Данные = Ответ.ПолучитьТелоКакДвоичныеДанные(); + ОбъявленнаяСумма = ЗначениеЗаголовка(Ответ.Заголовки, "X-Checksum-Sha256"); Ответ.Закрыть(); Лог.Отладка("Соединение закрыто"); + ПроверитьКонтрольнуюСумму(Данные, ОбъявленнаяСумма, ИмяПакета); + Данные.Записать(ПутьКФайлуПакета); Иначе - ТекстИсключения = СтрШаблон("Ошибка установки пакета %1: Нет соединения", ИмяПакета); + ТекстИсключения = СтрШаблон("Ошибка установки пакета %1: %2", ИмяПакета, ПричинаНеудачи(ФайлПакета, Полученное)); ВызватьИсключение ТекстИсключения; КонецЕсли; @@ -55,16 +59,77 @@ КонецФункции -Функция ЗапроситьПакет(Знач ПереченьСерверов, Знач ИмяСервера, Знач ИмяРесурса) +// Хаб без поддержки контрольной суммы заголовка не присылает — такой файл не сверяем. +Процедура ПроверитьКонтрольнуюСумму(Знач Данные, Знач ОбъявленнаяСумма, Знач ИмяПакета) + + Если ПустаяСтрока(ОбъявленнаяСумма) Тогда + Возврат; + КонецЕсли; + + Хеширование = Новый ХешированиеДанных(ХешФункция.SHA256); + Хеширование.Добавить(Данные); + СуммаФайла = НРег(ПолучитьHexСтрокуИзДвоичныхДанных(Хеширование.ХешСумма)); + + Если СуммаФайла <> НРег(ОбъявленнаяСумма) Тогда + ВызватьИсключение СтрШаблон("Ошибка установки пакета %1: контрольная сумма файла %2 не совпала с объявленной хабом %3. " + + "Файл повреждён при скачивании или подменён", ИмяПакета, СуммаФайла, ОбъявленнаяСумма); + КонецЕсли; + +КонецПроцедуры + +Функция ЗначениеЗаголовка(Знач Заголовки, Знач Имя) + + Для Каждого Заголовок Из Заголовки Цикл + Если НРег(Заголовок.Ключ) = НРег(Имя) Тогда + Возврат СокрЛП(Заголовок.Значение); + КонецЕсли; + КонецЦикла; + + Возврат ""; + +КонецФункции + +Функция ПричинаНеудачи(Знач ФайлПакета, Знач Полученное) + + КодСостояния = Полученное.КодСостояния; + Если СтрДлина(Полученное.ТелоОтвета) > 300 Тогда + Возврат Лев(Полученное.ТелоОтвета, 300) + "..."; + ИначеЕсли ЗначениеЗаполнено(Полученное.ТелоОтвета) Тогда + Возврат Полученное.ТелоОтвета; + ИначеЕсли КодСостояния = 404 Тогда + Возврат СтрШаблон("файл %1 в хабе не найден", ФайлПакета); + ИначеЕсли КодСостояния = 401 ИЛИ КодСостояния = 403 Тогда + Возврат СтрШаблон("хаб отказал в доступе к %1 (код %2)", ФайлПакета, Формат(КодСостояния, "ЧГ=0")); + ИначеЕсли КодСостояния <> 0 Тогда + Возврат СтрШаблон("хаб ответил кодом %1 на запрос %2", Формат(КодСостояния, "ЧГ=0"), ФайлПакета); + ИначеЕсли ЗначениеЗаполнено(Полученное.ОшибкаСети) Тогда + Возврат Полученное.ОшибкаСети; + КонецЕсли; + + Возврат "Нет соединения"; + +КонецФункции + +// Возвращаемое значение: +// Структура - Ответ (HTTPОтвет, Неопределено), КодСостояния (Число, 0 — ответа не было), +// ТелоОтвета (Строка - текст ответа с кодом не 200 без пробелов по краям), +// ОшибкаСети (Строка - сообщение о неудачном подключении к единственному названному серверу). +Функция ЗапроситьПакет(Знач ПереченьСерверов, Знач ИмяСервера, Знач ИмяРесурса, Знач Спецификация) ПакетУспешноПолучен = Ложь; ОтветСервера = Неопределено; + КодСостояния = 0; + ТелоОтвета = ""; + ОшибкаСети = ""; // Если указан нужный сервер, то только его и будем использовать Если ЗначениеЗаполнено(ИмяСервера) Тогда Сервер = ИндексСерверовПакетов[ИмяСервера]; - ОтветСервера = ЗапроситьПакетССервера(Сервер, ИмяРесурса); + ОтветСервера = ЗапроситьПакетССервера(Сервер, ИмяРесурса, Спецификация, КодСостояния, ТелоОтвета); + Если ОтветСервера = Неопределено И ТипЗнч(Сервер) = Тип("СерверПакетов") Тогда + ОшибкаСети = Сервер.ПоследняяОшибкаСети; + КонецЕсли; Иначе @@ -72,7 +137,7 @@ Для Каждого ДоступныйСервер Из ПереченьСерверов Цикл Сервер = ИндексСерверовПакетов[ДоступныйСервер.Ключ]; - ОтветСервера = ЗапроситьПакетССервера(Сервер, ИмяРесурса); + ОтветСервера = ЗапроситьПакетССервера(Сервер, ИмяРесурса, Спецификация, КодСостояния, ТелоОтвета); Если ОтветСервера <> Неопределено Тогда Прервать; @@ -86,33 +151,36 @@ Если ОтветСервера <> Неопределено Тогда Лог.Отладка("Ресурс %1 успешно получен с %2", ИмяРесурса, Сервер.ПолучитьИмя()); - Возврат ОтветСервера; КонецЕсли; - Возврат Неопределено; + Возврат Новый Структура("Ответ, КодСостояния, ТелоОтвета, ОшибкаСети", ОтветСервера, КодСостояния, ТелоОтвета, ОшибкаСети); КонецФункции -Функция ЗапроситьПакетССервера(Сервер, ИмяРесурса) +Функция ЗапроситьПакетССервера(Сервер, ИмяРесурса, Знач Спецификация, КодСостояния, ТелоОтвета) ОтветСервера = Неопределено; Если Сервер.СерверДоступен() Тогда - ОтветСервера = Сервер.ПолучитьРесурс(ИмяРесурса); + ОтветСервера = Сервер.ПолучитьРесурс(ИмяРесурса, Спецификация); Если ОтветСервера <> Неопределено Тогда + КодСостояния = ОтветСервера.КодСостояния; + Если ОтветСервера.КодСостояния = 200 Тогда Возврат ОтветСервера; КонецЕсли; + ТелоОтвета = СокрЛП(ОтветСервера.ПолучитьТелоКакСтроку()); ОтветСервера.Закрыть(); - Лог.Информация("Ошибка подключения к хабу %1 <%2>", + Лог.Отладка("Хаб %1 ответил кодом %2 на запрос %3", Сервер.ПолучитьИмя(), - ОтветСервера.КодСостояния); + КодСостояния, + ИмяРесурса); ОтветСервера = Неопределено; diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" index 368fe0a..e3f6e40 100644 --- "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -10,6 +10,7 @@ Перем Авторизация Экспорт; Перем ДополнительныеЗаголовки Экспорт; Перем Таймаут Экспорт; +Перем ПоследняяОшибкаСети Экспорт; Перем ПакетыХаба; @@ -26,6 +27,7 @@ Авторизация = ВходящаяАвторизация; ДополнительныеЗаголовки = Заголовки; Таймаут = ВходящийТаймаут; + ПоследняяОшибкаСети = ""; КонецПроцедуры @@ -55,14 +57,23 @@ КонецФункции -// ИмяРесурса - имя файла относительно "Сервер/ПутьВХранилище" -// Возвращает HttpОтвет или Неопределено, если запрос вернул исключение. -Функция ПолучитьРесурс(Знач ИмяРесурса) Экспорт +// Получает ресурс с сервера. +// +// Параметры: +// ИмяРесурса - Строка - имя файла относительно "Сервер/ПутьНаСервере". +// Спецификация - Строка, Неопределено - строка после @, уходит параметром запроса spec; +// пустая параметра не добавляет. +// +// Возвращаемое значение: +// HTTPОтвет - ответ сервера; Неопределено, если запрос завершился исключением. +// +Функция ПолучитьРесурс(Знач ИмяРесурса, Знач Спецификация = Неопределено) Экспорт Соединение = ИнициализироватьСоединение(); - Ресурс = ПутьНаСервере + ИмяРесурса; + Ресурс = АдресРесурса(ИмяРесурса, Спецификация); Запрос = Новый HTTPЗапрос(Ресурс); ДобавитьЗаголовки(Запрос); + ПоследняяОшибкаСети = ""; Попытка @@ -70,13 +81,136 @@ Исключение - Лог.Ошибка(ОписаниеОшибки()); + ПоследняяОшибкаСети = СтрШаблон("Не удалось подключиться к %1", АдресДляСообщений()); + Лог.Отладка("Обращение к %1%2 не удалось: %3", Сервер, Ресурс, ОписаниеОшибки()); Возврат Неопределено; КонецПопытки; КонецФункции +// Адрес ресурса в запросе к серверу: путь с закодированными сегментами и строка запроса. +// +// Параметры: +// ИмяРесурса - Строка - имя файла относительно "Сервер/ПутьНаСервере". +// Спецификация - Строка, Неопределено - строка после @; пустая параметра spec не добавляет. +// К файлу пакета добавляется параметр engine - версия установленного движка. +// +// Возвращаемое значение: +// Строка - путь и, если есть параметры, строка запроса. +// +Функция АдресРесурса(Знач ИмяРесурса, Знач Спецификация = Неопределено) Экспорт + + Сегменты = СтрРазделить(ИмяРесурса, "/"); + Для Индекс = 0 По Сегменты.ВГраница() Цикл + Сегменты[Индекс] = ЧастьАдреса(Сегменты[Индекс]); + КонецЦикла; + + ПараметрыЗапроса = Новый Массив; + Если ЗначениеЗаполнено(Спецификация) Тогда + ПараметрыЗапроса.Добавить("spec=" + ЧастьАдреса(Спецификация)); + КонецЕсли; + Если СтрЗаканчиваетсяНа(ИмяРесурса, ".ospx") Тогда + ПараметрыЗапроса.Добавить("engine=" + ЧастьАдреса(РаботаСОписаниемПакета.ВерсияДвижка())); + КонецЕсли; + + Адрес = ПутьНаСервере + СтрСоединить(Сегменты, "/"); + Если ПараметрыЗапроса.Количество() = 0 Тогда + Возврат Адрес; + КонецЕсли; + + Возврат Адрес + "?" + СтрСоединить(ПараметрыЗапроса, "&"); + +КонецФункции + +// Публикует файл пакета; отказ сервера - исключение с его сообщением. +// +// Параметры: +// ФайлПакета - Файл - файл .ospx. +// Канал - Строка - значение заголовка CHANNEL. +// Токен - Строка - токен публикации; пустой - без токена. +// +// Возвращаемое значение: +// Строка - сообщение сервера. +// +Функция ОтправитьПакет(Знач ФайлПакета, Знач Канал, Знач Токен) Экспорт + + Соединение = ИнициализироватьСоединение(); + Запрос = ЗапросПубликации(ФайлПакета, Канал, Токен); + + Попытка + Ответ = Соединение.ОтправитьДляОбработки(Запрос); + Исключение + Лог.Отладка("Публикация в %1%2 не удалась: %3", Сервер, РесурсПубликацииПакетов, ОписаниеОшибки()); + ВызватьИсключение СтрШаблон("Не удалось подключиться к %1", АдресДляСообщений()); + КонецПопытки; + + Сообщение = СообщениеСервера(Ответ.ПолучитьТелоКакСтроку()); + Если Не КодУспешнойОтправки(Ответ.КодСостояния) Тогда + ВызватьИсключение Сообщение; + КонецЕсли; + + Возврат Сообщение; + +КонецФункции + +// Токен уходит заголовком OAUTH-TOKEN, если у сервера нет своей авторизации. +Функция ЗапросПубликации(Знач ФайлПакета, Знач Канал, Знач Токен) + + Запрос = Новый HTTPЗапрос(РесурсПубликацииПакетов); + Запрос.Заголовки.Вставить("FILE-NAME", ФайлПакета.Имя); + Запрос.Заголовки.Вставить("CHANNEL", Канал); + Если ЗначениеЗаполнено(Токен) И Не ЗначениеЗаполнено(Авторизация) Тогда + Запрос.Заголовки.Вставить("OAUTH-TOKEN", Токен); + КонецЕсли; + ДобавитьЗаголовки(Запрос); + Запрос.УстановитьТелоИзДвоичныхДанных(Новый ДвоичныеДанные(ФайлПакета.ПолноеИмя)); + + Возврат Запрос; + +КонецФункции + +// Хаб OpenHub отвечает JSON {"КодСостояние", "Сообщение"}: пользователю - только сообщение. +Функция СообщениеСервера(Знач ТелоОтвета) + + Попытка + Чтение = Новый ЧтениеJSON(); + Чтение.УстановитьСтроку(ТелоОтвета); + Ответ = ПрочитатьJSON(Чтение, Истина); + Чтение.Закрыть(); + Исключение + Возврат ТелоОтвета; + КонецПопытки; + + Если ТипЗнч(Ответ) = Тип("Соответствие") И Ответ["Сообщение"] <> Неопределено Тогда + Возврат Ответ["Сообщение"]; + КонецЕсли; + + Возврат ТелоОтвета; + +КонецФункции + +Функция КодУспешнойОтправки(Знач КодСостояния) + Возврат КодСостояния = 200 Или КодСостояния = 201 Или КодСостояния = 202; +КонецФункции + +// Платформа оставляет плюс в адресе как есть, а в строке запроса он читается пробелом. +Функция ЧастьАдреса(Знач Значение) + Возврат КодироватьСтроку(Значение, СпособКодированияСтроки.КодировкаURL); +КонецФункции + +// Адрес сервера в сообщениях пользователю: порт указывается, только если он не стандартный. +Функция АдресДляСообщений() Экспорт + + СтандартныйПорт = ?(СтрНачинаетсяС(НРег(Сервер), "https://"), 443, 80); + Если Порт = 0 ИЛИ Порт = СтандартныйПорт Тогда + Возврат Сервер; + КонецЕсли; + + Возврат СтрШаблон("%1:%2", Сервер, Формат(Порт, "ЧГ=0")); + +КонецФункции + Функция ПрочитатьФайлСпискаПакетов(Текст) ТекстовыйДокумент = Новый ТекстовыйДокумент; @@ -101,15 +235,14 @@ Ответ = ПолучитьРесурс(Ресурс); Если Ответ = Неопределено Тогда - ТекстИсключения = "Ошибка подключения к зеркалу"; - ВызватьИсключение ТекстИсключения; + ВызватьИсключение ПоследняяОшибкаСети; КонецЕсли; Если Ответ.КодСостояния <> 200 Тогда ТекстОтвета = Ответ.ПолучитьТелоКакСтроку(); - ТекстИсключения = СтрШаблон("Ошибка подключения к зеркалу код ответа: <%1> - |Текст ответа: <%2>", Ответ.КодСостояния, ТекстОтвета); - ВызватьИсключение ТекстИсключения; + Ответ.Закрыть(); + ВызватьИсключение СтрШаблон("%1 ответил кодом %2 на запрос списка пакетов: %3", + АдресДляСообщений(), Формат(Ответ.КодСостояния, "ЧГ=0"), СокрЛП(ТекстОтвета)); КонецЕсли; ТекстОтвета = Ответ.ПолучитьТелоКакСтроку(); Ответ.Закрыть(); diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\236\321\202\320\262\320\265\321\202.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\236\321\202\320\262\320\265\321\202.os" index 9df20eb..a341e26 100644 --- "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\236\321\202\320\262\320\265\321\202.os" +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\236\321\202\320\262\320\265\321\202.os" @@ -2,11 +2,13 @@ Перем Лог; Перем КодСостояния Экспорт; +Перем Заголовки Экспорт; Перем ИмяФайла; -Процедура ПриСозданииОбъекта(Знач ПутьКФайлу) +Процедура ПриСозданииОбъекта(Знач ПутьКФайлу, Знач ПодбиратьВерсию = Истина) ИмяФайла = ПутьКФайлу; + Заголовки = Новый Соответствие(); Файл = Новый Файл(ПутьКФайлу); Если Файл.Существует() Тогда @@ -15,7 +17,7 @@ КодСостояния = 404; - Если СтрЗаканчиваетсяНа(ПутьКФайлу, ".ospx") Тогда + Если ПодбиратьВерсию И СтрЗаканчиваетсяНа(ПутьКФайлу, ".ospx") Тогда ПодобратьФайлПакета(Файл.Путь); КонецЕсли; @@ -27,6 +29,10 @@ Возврат Новый ДвоичныеДанные(ИмяФайла); КонецФункции +Функция ПолучитьТелоКакСтроку() Экспорт + Возврат ""; +КонецФункции + Процедура Закрыть() Экспорт // для совместимости контракта КонецПроцедуры diff --git "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" index fceeaea..e7c26d8 100644 --- "a/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" +++ "b/src/core/\320\232\320\273\320\260\321\201\321\201\321\213/\320\244\320\260\320\271\320\273\320\276\320\262\321\213\320\271\320\241\320\265\321\200\320\262\320\265\321\200\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -33,17 +33,18 @@ // // Параметры: // ИмяРесурса - Строка - имя файла относительно "Сервер/ПутьВХранилище" +// Спецификация - Строка, Неопределено - строка после @; названная запрещает подбор другой версии пакета // // Возвращаемое значение: // ФайловыйСерверОтвет - Если файл ресурса найден // Неопределено - Если файл ресурса не найден или возникла ошибка обращения к файлам -Функция ПолучитьРесурс(Знач ИмяРесурса) Экспорт +Функция ПолучитьРесурс(Знач ИмяРесурса, Знач Спецификация = Неопределено) Экспорт ПутьКРесурсу = ОбъединитьПути(КорневойКаталогПоиска, ИмяРесурса); Попытка - Возврат Новый ФайловыйСерверОтвет(ПутьКРесурсу); + Возврат Новый ФайловыйСерверОтвет(ПутьКРесурсу, Спецификация = Неопределено); Исключение diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\220\320\264\321\200\320\265\321\201\320\260\320\245\320\260\320\261\320\260.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\220\320\264\321\200\320\265\321\201\320\260\320\245\320\260\320\261\320\260.os" new file mode 100644 index 0000000..6af4947 --- /dev/null +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\220\320\264\321\200\320\265\321\201\320\260\320\245\320\260\320\261\320\260.os" @@ -0,0 +1,112 @@ +// Адрес хаба в командной строке: [схема://]хост[:порт][/пул][/пакет[@спецификация]] либо хаб и пул по отдельности. + +Перем ФорматАдреса; + +// Похожа ли строка на адрес хаба, а не на имя пакета: есть схема либо путь после хоста, +// у которого есть порт, точка в имени или это localhost. +// +// Параметры: +// Значение - Строка - аргумент команды. +// +// Возвращаемое значение: +// Булево - Истина, если строка - адрес хаба. +// +Функция ЭтоАдресХаба(Знач Значение) Экспорт + + Части = ЧастиАдреса(Значение); + Если Части = Неопределено Тогда + Возврат Ложь; + КонецЕсли; + + ХостЯвный = ЗначениеЗаполнено(Части.Порт) Или Части.Хост = "localhost" Или СтрНайти(Части.Хост, ".") > 1; + Возврат ЗначениеЗаполнено(Части.Схема) Или (ЗначениеЗаполнено(Части.Путь) И ХостЯвный); + +КонецФункции + +// Разбирает адрес пакета: последний сегмент пути - пакет, сегмент перед ним - пул. +// +// Параметры: +// Адрес - Строка - [схема://]хост[:порт]/[пул/]пакет[@спецификация]. +// +// Возвращаемое значение: +// Структура - Сервер (Строка - схема и хост), Порт (Число, 0 - порт схемы), +// Пул (Строка, пустая - пул хаба по умолчанию), Пакет (Строка - имя и спецификация после @). +// +Функция АдресПакета(Знач Адрес) Экспорт + + Результат = Разобрать(Адрес, "[схема://]хост[:порт]/[пул/]пакет[@спецификация]", 1, 2); + Результат.Вставить("Пакет", Результат.Сегменты[Результат.Сегменты.ВГраница()]); + Если Результат.Сегменты.Количество() = 2 Тогда + Результат.Пул = Результат.Сегменты[0]; + КонецЕсли; + + Возврат Результат; + +КонецФункции + +// Собирает адрес пула из хаба и пула, заданных по отдельности. +// +// Параметры: +// Хаб - Строка - [схема://]хост[:порт] без пути; пустой - хаб по умолчанию. +// Пул - Строка - имя пула; пустое - пул хаба по умолчанию. +// +// Возвращаемое значение: +// Структура - Сервер (Строка - схема и хост), Порт (Число, 0 - порт схемы), +// Пул (Строка, пустая - пул хаба по умолчанию). +// +Функция Адрес(Знач Хаб, Знач Пул = "") Экспорт + + Если ПустаяСтрока(Хаб) Тогда + Хаб = КонстантыOpm.СерверУдаленногоХранилища; + КонецЕсли; + + Результат = Разобрать(Хаб, "[схема://]хост[:порт] без пути, пул задаётся опцией --pool", 0, 0); + Результат.Пул = СокрЛП(Пул); + + Возврат Результат; + +КонецФункции + +Функция Разобрать(Знач Адрес, Знач Форма, Знач НаименьшееЧислоСегментов, Знач НаибольшееЧислоСегментов) + + Части = ЧастиАдреса(Адрес); + Если Части <> Неопределено Тогда + Сегменты = СтрРазделить(Части.Путь, "/", Ложь); + КонецЕсли; + + Если Части = Неопределено + Или Сегменты.Количество() < НаименьшееЧислоСегментов + Или Сегменты.Количество() > НаибольшееЧислоСегментов Тогда + ВызватьИсключение СтрШаблон("Адрес <%1> не разобран: ожидалось %2", Адрес, Форма); + КонецЕсли; + + Схема = Части.Схема; + Если ПустаяСтрока(Схема) Тогда + // без схемы открытый канал - только для своей машины и адреса с явным портом + ЭтотКомпьютер = Части.Хост = "localhost" Или СтрНачинаетсяС(Части.Хост, "127."); + Схема = ?(ЭтотКомпьютер Или ЗначениеЗаполнено(Части.Порт), "http", "https"); + КонецЕсли; + + Возврат Новый Структура("Сервер, Порт, Пул, Сегменты", + НРег(Схема) + "://" + Части.Хост, + ?(ЗначениеЗаполнено(Части.Порт), Число(Части.Порт), 0), + "", + Сегменты); + +КонецФункции + +Функция ЧастиАдреса(Знач Адрес) + + Совпадения = ФорматАдреса.НайтиСовпадения(СокрЛП(Адрес)); + Если Совпадения.Количество() = 0 Тогда + Возврат Неопределено; + КонецЕсли; + + Группы = Совпадения[0].Группы; + Возврат Новый Структура("Схема, Хост, Порт, Путь", + Группы[1].Значение, НРег(Группы[2].Значение), Группы[3].Значение, Группы[4].Значение); + +КонецФункции + +ФорматАдреса = Новый РегулярноеВыражение("^(?:(https?)://)?([^/:]+)(?::(\d+))?(/.*)?$"); +ФорматАдреса.ИгнорироватьРегистр = Истина; diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\236\320\277\320\270\321\201\320\260\320\275\320\270\320\265\320\274\320\237\320\260\320\272\320\265\321\202\320\260.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\236\320\277\320\270\321\201\320\260\320\275\320\270\320\265\320\274\320\237\320\260\320\272\320\265\321\202\320\260.os" index 728349b..5cda1a6 100644 --- "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\236\320\277\320\270\321\201\320\260\320\275\320\270\320\265\320\274\320\237\320\260\320\272\320\265\321\202\320\260.os" +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\236\320\277\320\270\321\201\320\260\320\275\320\270\320\265\320\274\320\237\320\260\320\272\320\265\321\202\320\260.os" @@ -59,8 +59,7 @@ ИмяПакета = Свойства.Имя; ТребуемаяВерсияСреды = Свойства.ВерсияСреды; - СистемнаяИнформация = Новый СистемнаяИнформация; - ВерсияСреды = СистемнаяИнформация.Версия; + ВерсияСреды = ВерсияДвижка(); Лог().Отладка("ПроверитьВерсиюСреды: Перед вызовом СравнитьВерсии(ЭтаВерсия = <%1>, БольшеЧемВерсия = <%2>)", ТребуемаяВерсияСреды, ВерсияСреды); Попытка @@ -86,6 +85,15 @@ КонецПроцедуры +// Версия установленного движка OneScript. +// +// Возвращаемое значение: +// Строка - версия, например 2.0.0. +// +Функция ВерсияДвижка() Экспорт + Возврат Новый СистемнаяИнформация().Версия; +КонецФункции + Функция Лог() Если Лог = Неопределено Тогда Лог = Логирование.ПолучитьЛог("oscript.app.opm"); diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\237\320\260\320\272\320\265\321\202\320\260\320\274\320\270.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\237\320\260\320\272\320\265\321\202\320\260\320\274\320\270.os" index bf36600..afb5dcb 100644 --- "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\237\320\260\320\272\320\265\321\202\320\260\320\274\320\270.os" +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\237\320\260\320\272\320\265\321\202\320\260\320\274\320\270.os" @@ -87,20 +87,48 @@ КонецФункции +// Добавляет в настройки сервер пула по адресу хаба: пакеты и их зависимости ставятся с него по имени сервера. +// +// Параметры: +// Адрес - Структура - результат АдресаХаба.АдресПакета или АдресаХаба.Адрес. +// ПоВсемПулам - Булево - выдача по всем доступным пулам хаба вместо пула адреса. +// +// Возвращаемое значение: +// Строка - имя сервера в настройках. +// +Функция ПодключитьСерверХаба(Знач Адрес, Знач ПоВсемПулам = Ложь) Экспорт + + Сервер = СерверыПакетов.ИзАдреса(Адрес, ПоВсемПулам); + НастройкиOpm.ДобавитьСерверПакетов(Сервер); + ВнутреннийМенеджерПолученияПакетов = Неопределено; + + Возврат Сервер.ПолучитьИмя(); + +КонецФункции + Процедура УстановитьПакетИзОблака(Знач ИмяПакета, Знач РежимУстановки, Знач ЦелевойКаталог, Знач НастройкаУстановки = Неопределено) Экспорт Лог.Отладка("Устанавливаю пакет <%1> из доступного хаба", ИмяПакета); + + Если НастройкаУстановки = Неопределено Тогда + НастройкаУстановки = ПолучитьНастройкуУстановки(); + КонецЕсли; + ИмяСервера = НастройкаУстановки.ИмяСервера; + + // пакет и его зависимости ставятся с сервера из адреса + Если АдресаХаба.ЭтоАдресХаба(ИмяПакета) Тогда + Адрес = АдресаХаба.АдресПакета(ИмяПакета); + ИмяСервера = ПодключитьСерверХаба(Адрес); + ИмяПакета = Адрес.Пакет; + КонецЕсли; + ИмяВерсияПакета = РаботаСВерсиями.РазобратьИмяПакета(ИмяПакета); ЭтоЗависимость = Ложь; Если РежимУстановки = РежимУстановкиПакетов.Локально Тогда ЭтоЗависимость = Истина; КонеЦесли; - Если НастройкаУстановки = Неопределено Тогда - НастройкаУстановки = ПолучитьНастройкуУстановки(); - КонецЕсли; - - МенеджерУстановки = Новый МенеджерУстановкиПакетов(РежимУстановки, ЦелевойКаталог, , НастройкаУстановки.ИмяСервера); + МенеджерУстановки = Новый МенеджерУстановкиПакетов(РежимУстановки, ЦелевойКаталог, , ИмяСервера); МенеджерУстановки.УстанавливатьЗависимости(НастройкаУстановки.УстанавливатьЗависимости); МенеджерУстановки.СоздаватьФайлыЗапуска(НастройкаУстановки.СоздаватьФайлыЗапуска); МенеджерУстановки.УстанавливатьЗависимостиРазработчика(НастройкаУстановки.УстанавливатьЗависимостиРазработчика); @@ -226,10 +254,22 @@ КонецПроцедуры -Функция ПолучитьПакет(Знач ИмяПакета, Знач ВерсияПакета, ПутьКФайлуПакета = "", ИмяСервера = "") Экспорт +// Скачивает файл пакета с сервера пакетов. +// +// Параметры: +// ИмяПакета - Строка - имя пакета. +// Спецификация - Строка, Неопределено - строка после @: точный номер версии, имя тега или выражение версии; +// Неопределено — версию выбирает сервер. +// ПутьКФайлуПакета - Строка - куда положить файл; пустая строка — во временный файл. +// ИмяСервера - Строка - имя сервера пакетов; пустая строка — перебор всех серверов. +// +// Возвращаемое значение: +// Строка - путь к скачанному файлу пакета. +// +Функция ПолучитьПакет(Знач ИмяПакета, Знач Спецификация, ПутьКФайлуПакета = "", ИмяСервера = "") Экспорт МенеджерПолучения = ПолучитьМенеджерПолученияПакетов(); - ПутьКФайлуПакета = МенеджерПолучения.ПолучитьПакет(ИмяПакета, ВерсияПакета, ПутьКФайлуПакета, ИмяСервера); + ПутьКФайлуПакета = МенеджерПолучения.ПолучитьПакет(ИмяПакета, Спецификация, ПутьКФайлуПакета, ИмяСервера); Возврат ПутьКФайлуПакета; diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\241\320\265\321\200\320\262\320\265\321\200\321\213\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\241\320\265\321\200\320\262\320\265\321\200\321\213\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" index d44abf1..b5a5a91 100644 --- "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\241\320\265\321\200\320\262\320\265\321\200\321\213\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\241\320\265\321\200\320\262\320\265\321\200\321\213\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -27,6 +27,86 @@ КонецФункции +// Сервер пакетов по адресу хаба из командной строки. Авторизация берётся у сервера +// из настроек с тем же адресом и портом. +// +// Параметры: +// Адрес - Структура - результат АдресаХаба.АдресПакета или АдресаХаба.Адрес. +// ПоВсемПулам - Булево - выдача по всем доступным пулам хаба вместо пула адреса. +// +// Возвращаемое значение: +// СерверПакетов - сервер выдачи и публикации пула. +// +Функция ИзАдреса(Знач Адрес, Знач ПоВсемПулам = Ложь) Экспорт + + Имя = Адрес.Сервер + ?(Адрес.Порт = 0, "", ":" + Формат(Адрес.Порт, "ЧГ=0")); + ПутьВыдачи = "/download/"; + ПутьПубликации = "/push"; + Если ПоВсемПулам Тогда + Имя = Имя + "/*"; + ПутьВыдачи = "/api/v1/download/"; + ИначеЕсли ЗначениеЗаполнено(Адрес.Пул) Тогда + Имя = Имя + "/" + Адрес.Пул; + ПутьВыдачи = СтрШаблон("/api/v1/pools/%1/download/", Адрес.Пул); + ПутьПубликации = СтрШаблон("/api/v1/pools/%1/push", Адрес.Пул); + КонецЕсли; + + Авторизация = ""; + Для Каждого Настройка Из НастройкиOpm.ПолучитьНастройки().СервераПакетов Цикл + Если ТипЗнч(Настройка) = Тип("СерверПакетов") И ЗначениеЗаполнено(Настройка.Авторизация) + И НРег(Настройка.Сервер) = Адрес.Сервер И Настройка.Порт = Адрес.Порт Тогда + Авторизация = Настройка.Авторизация; + Прервать; + КонецЕсли; + КонецЦикла; + + Возврат Новый СерверПакетов(Имя, Адрес.Сервер, ПутьВыдачи, ПутьПубликации, Адрес.Порт, 0, + Авторизация, Новый Соответствие, 600); + +КонецФункции + +// Сервер публикации: пул хаба по адресу и пулу (пустой адрес - хаб по умолчанию), +// сервер из настроек по имени либо хаб по умолчанию. +// +// Параметры: +// АдресХаба - Строка - [схема://]хост[:порт]; пустой без пула - публикация на сервер по имени. +// ИмяСервераПакетов - Строка - имя сервера из настроек. +// Пул - Строка - пул хаба из АдресХаба; пустой - пул хаба по умолчанию. +// Форсировать - Булево - опубликовать поверх уже занятого номера версии. +// +// Возвращаемое значение: +// СерверПакетов - сервер с ресурсом публикации. +// +Функция ДляПубликации(Знач АдресХаба, Знач ИмяСервераПакетов, Знач Пул = "", Знач Форсировать = Ложь) Экспорт + + Сервер = СерверПубликации(АдресХаба, ИмяСервераПакетов, Пул); + Если Форсировать Тогда + Сервер.РесурсПубликацииПакетов = Сервер.РесурсПубликацииПакетов + + ?(СтрНайти(Сервер.РесурсПубликацииПакетов, "?") > 0, "&", "?") + "force=1"; + КонецЕсли; + + Возврат Сервер; + +КонецФункции + +// force=1 в ресурсе публикации просит хаб опубликовать поверх занятого номера версии. +Функция СерверПубликации(Знач АдресХаба, Знач ИмяСервераПакетов, Знач Пул) + + Если ЗначениеЗаполнено(АдресХаба) Или ЗначениеЗаполнено(Пул) Тогда + Возврат ИзАдреса(АдресаХаба.Адрес(АдресХаба, Пул)); + КонецЕсли; + + Для Каждого НастройкаСервера Из НастройкиOpm.ПолучитьНастройки().СервераПакетов Цикл + Если СтрСравнить(НастройкаСервера.Имя, ИмяСервераПакетов) = 0 Тогда + Возврат НастройкаСервера; + КонецЕсли; + КонецЦикла; + + Возврат Новый СерверПакетов(КонстантыOpm.СерверУдаленногоХранилища, КонстантыOpm.СерверУдаленногоХранилища, , + КонстантыOpm.РесурсПубликацииПакетов, 0, 0, , , 600); + +КонецФункции + Функция ОсновнойСервер() Экспорт Возврат Новый СерверПакетов("ОсновнойСерверПакетов", КонстантыOpm.СерверУдаленногоХранилища, diff --git "a/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\242\320\276\320\272\320\265\320\275\320\232\320\276\320\275\320\262\320\265\320\271\320\265\321\200\320\260CI.os" "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\242\320\276\320\272\320\265\320\275\320\232\320\276\320\275\320\262\320\265\320\271\320\265\321\200\320\260CI.os" new file mode 100644 index 0000000..5523af9 --- /dev/null +++ "b/src/core/\320\234\320\276\320\264\321\203\320\273\320\270/\320\242\320\276\320\272\320\265\320\275\320\232\320\276\320\275\320\262\320\265\320\271\320\265\321\200\320\260CI.os" @@ -0,0 +1,69 @@ +#Использовать logos + +// id-token конвейера GitHub Actions или GitLab CI: хаб принимает его вместо токена публикации. + +// Получает id-token конвейера: из переменной OPENHUB_TOKEN (GitLab CI) либо у сервиса токенов GitHub Actions. +// +// Параметры: +// Сервер - СерверПакетов - сервер публикации; его адрес - аудитория по умолчанию. +// Аудитория - Строка - аудитория id-token в GitHub Actions; пустая - адрес сервера. +// Соединение - HTTPСоединение - соединение с сервисом токенов GitHub; Неопределено - по его адресу. +// +// Возвращаемое значение: +// Строка - id-token. +// +Функция Получить(Знач Сервер, Знач Аудитория = "", Знач Соединение = Неопределено) Экспорт + + Токен = ПолучитьПеременнуюСреды("OPENHUB_TOKEN"); + Если ЗначениеЗаполнено(Токен) Тогда + Возврат Токен; + КонецЕсли; + + АдресСервиса = ПолучитьПеременнуюСреды("ACTIONS_ID_TOKEN_REQUEST_URL"); + ТокенЗапроса = ПолучитьПеременнуюСреды("ACTIONS_ID_TOKEN_REQUEST_TOKEN"); + Если Не ЗначениеЗаполнено(АдресСервиса) Или Не ЗначениеЗаполнено(ТокенЗапроса) Тогда + ВызватьИсключение "Конвейер не выдал id-token: в GitHub Actions нужно permissions: id-token: write," + + " в GitLab CI - переменная OPENHUB_TOKEN в блоке id_tokens"; + КонецЕсли; + + Если Не ЗначениеЗаполнено(Аудитория) Тогда + Аудитория = Сервер.АдресДляСообщений(); + КонецЕсли; + + КонецХоста = СтрНайти(АдресСервиса, "/", , СтрНайти(АдресСервиса, "://") + 3); + Хост = Лев(АдресСервиса, КонецХоста - 1); + Ресурс = Сред(АдресСервиса, КонецХоста); + Ресурс = Ресурс + ?(СтрНайти(Ресурс, "?") > 0, "&", "?") + + "audience=" + КодироватьСтроку(Аудитория, СпособКодированияСтроки.КодировкаURL); + + Запрос = Новый HTTPЗапрос(Ресурс); + Запрос.Заголовки.Вставить("Authorization", "bearer " + ТокенЗапроса); + + Если Соединение = Неопределено Тогда + Соединение = Новый HTTPСоединение(Хост); + КонецЕсли; + + Попытка + Ответ = Соединение.Получить(Запрос); + Исключение + Логирование.ПолучитьЛог("oscript.app.opm").Отладка("Запрос id-token к %1 не удался: %2", Хост, ОписаниеОшибки()); + ВызватьИсключение СтрШаблон("Не удалось подключиться к %1", Хост); + КонецПопытки; + + Тело = Ответ.ПолучитьТелоКакСтроку(); + Если Ответ.КодСостояния <> 200 Тогда + ВызватьИсключение СтрШаблон("Сервис id-token GitHub ответил кодом %1: %2", Ответ.КодСостояния, Тело); + КонецЕсли; + + Чтение = Новый ЧтениеJSON(); + Чтение.УстановитьСтроку(Тело); + Токен = ПрочитатьJSON(Чтение, Истина)["value"]; + Чтение.Закрыть(); + + Если Не ЗначениеЗаполнено(Токен) Тогда + ВызватьИсключение "В ответе сервиса id-token GitHub нет поля value"; + КонецЕсли; + + Возврат Токен; + +КонецФункции diff --git a/tests/checksum-test.os b/tests/checksum-test.os new file mode 100644 index 0000000..1be6931 --- /dev/null +++ b/tests/checksum-test.os @@ -0,0 +1,91 @@ +#Использовать asserts +#Использовать tempfiles +#Использовать "../src/core" + +Перем юТест; +Перем Хаб; +Перем Ответ; +Перем ФайлПакета; + +// sha256 тела заглушки ответа «файл пакета» в UTF-8 +Перем СуммаТела; + +Функция ПолучитьСписокТестов(Знач Тестирование) Экспорт + + юТест = Тестирование; + + СписокТестов = Новый Массив; + + СписокТестов.Добавить("ТестДолжен_ПринятьФайлССовпавшейСуммой"); + СписокТестов.Добавить("ТестДолжен_ОтказатьПриРасхожденииСуммы"); + СписокТестов.Добавить("ТестДолжен_ПропуститьСверкуБезЗаголовка"); + + Возврат СписокТестов; + +КонецФункции + +Процедура ПередЗапускомТеста() Экспорт + + СуммаТела = "4d7a8fcdfff25ff76d6eb81129d1f6d8b655e927458293b9f14062c9dd5b6117"; + + Ответ = ЗагрузитьЗаглушку("ЗаглушкаОтветаХаба.os"); + Хаб = ЗагрузитьЗаглушку("ЗаглушкаСервераПакетов.os"); + Хаб.ОбъявитьПакет("vrunner"); + Хаб.Отвечать(Ответ); + + НастройкиOpm.СброситьНастройки(); + НастройкиOpm.ДобавитьСерверПакетов(Хаб); + + ФайлПакета = ВременныеФайлы.НовоеИмяФайла("ospx"); + +КонецПроцедуры + +Процедура ПослеЗапускаТеста() Экспорт + + НастройкиOpm.СброситьНастройки(); + ВременныеФайлы.Удалить(); + +КонецПроцедуры + +Процедура ТестДолжен_ПринятьФайлССовпавшейСуммой() Экспорт + + Ответ.Заголовки.Вставить("x-checksum-sha256", ВРег(СуммаТела)); + + Скачать(); + + Ожидаем.Что(Новый Файл(ФайлПакета).Существует(), "файл записан").ЭтоИстина(); + +КонецПроцедуры + +Процедура ТестДолжен_ОтказатьПриРасхожденииСуммы() Экспорт + + Ответ.Заголовки.Вставить("X-Checksum-Sha256", СтрСоединить(Новый Массив(65), "0")); + + Попытка + Скачать(); + Исключение + Ожидаем.Что(ИнформацияОбОшибке().Описание, "причина отказа").Содержит("контрольная сумма файла " + СуммаТела); + Ожидаем.Что(Новый Файл(ФайлПакета).Существует(), "файл не записан").ЭтоЛожь(); + Возврат; + КонецПопытки; + + юТест.ПрерватьТест("Ожидали отказ при расхождении контрольной суммы"); + +КонецПроцедуры + +Процедура ТестДолжен_ПропуститьСверкуБезЗаголовка() Экспорт + + Скачать(); + + Ожидаем.Что(Новый Файл(ФайлПакета).Существует(), "файл записан").ЭтоИстина(); + +КонецПроцедуры + +Процедура Скачать() + МенеджерПолучения = Новый МенеджерПолученияПакетов(); + МенеджерПолучения.ПолучитьПакет("vrunner", Неопределено, ФайлПакета); +КонецПроцедуры + +Функция ЗагрузитьЗаглушку(Знач ИмяФайла) + Возврат ЗагрузитьСценарий(ОбъединитьПути(ТекущийСценарий().Каталог, "fixtures", ИмяФайла)); +КонецФункции diff --git "a/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\232\320\276\320\275\320\262\320\265\320\271\320\265\321\200\320\260.os" "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\232\320\276\320\275\320\262\320\265\320\271\320\265\321\200\320\260.os" new file mode 100644 index 0000000..99b4d34 --- /dev/null +++ "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\232\320\276\320\275\320\262\320\265\320\271\320\265\321\200\320\260.os" @@ -0,0 +1,9 @@ +// Соединение с сервисом токенов GitHub: запоминает запрос и отдаёт заданный ответ. + +Перем Запрос Экспорт; +Перем Ответ Экспорт; + +Функция Получить(Знач ВходящийЗапрос) Экспорт + Запрос = ВходящийЗапрос; + Возврат Ответ; +КонецФункции diff --git "a/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\236\321\202\320\262\320\265\321\202\320\260\320\245\320\260\320\261\320\260.os" "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\236\321\202\320\262\320\265\321\202\320\260\320\245\320\260\320\261\320\260.os" new file mode 100644 index 0000000..a543df5 --- /dev/null +++ "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\236\321\202\320\262\320\265\321\202\320\260\320\245\320\260\320\261\320\260.os" @@ -0,0 +1,20 @@ +// Ответ хаба с заданным кодом, текстом и заголовками. + +Перем КодСостояния Экспорт; +Перем Тело Экспорт; +Перем Заголовки Экспорт; + +Функция ПолучитьТелоКакДвоичныеДанные() Экспорт + Возврат ПолучитьДвоичныеДанныеИзСтроки("файл пакета"); +КонецФункции + +Функция ПолучитьТелоКакСтроку() Экспорт + Возврат Тело; +КонецФункции + +Процедура Закрыть() Экспорт +КонецПроцедуры + +КодСостояния = 200; +Тело = ""; +Заголовки = Новый Соответствие(); diff --git "a/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\241\320\265\321\200\320\262\320\265\321\200\320\260\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\241\320\265\321\200\320\262\320\265\321\200\320\260\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" new file mode 100644 index 0000000..3a13529 --- /dev/null +++ "b/tests/fixtures/\320\227\320\260\320\263\320\273\321\203\321\210\320\272\320\260\320\241\320\265\321\200\320\262\320\265\321\200\320\260\320\237\320\260\320\272\320\265\321\202\320\276\320\262.os" @@ -0,0 +1,42 @@ +// Сервер пакетов без сети: запоминает запросы и отвечает заданным ответом. + +Перем Имя Экспорт; +Перем Запросы Экспорт; + +Перем Пакеты; +Перем Ответ; + +Процедура ПриСозданииОбъекта(Знач ИмяСервера = "ЗаглушкаХаба") + Имя = ИмяСервера; + Пакеты = Новый Соответствие(); + Запросы = Новый Массив(); +КонецПроцедуры + +Процедура ОбъявитьПакет(Знач ИмяПакета) Экспорт + Пакеты.Вставить(ИмяПакета, ""); +КонецПроцедуры + +Процедура Отвечать(Знач ОтветХаба) Экспорт + Ответ = ОтветХаба; +КонецПроцедуры + +Функция ПоследнийЗапрос() Экспорт + Возврат Запросы[Запросы.ВГраница()]; +КонецФункции + +Функция ПолучитьИмя() Экспорт + Возврат Имя; +КонецФункции + +Функция СерверДоступен() Экспорт + Возврат Истина; +КонецФункции + +Функция ПолучитьПакеты() Экспорт + Возврат Пакеты; +КонецФункции + +Функция ПолучитьРесурс(Знач ИмяРесурса, Знач Спецификация = Неопределено) Экспорт + Запросы.Добавить(Новый Структура("ИмяРесурса, Спецификация", ИмяРесурса, Спецификация)); + Возврат Ответ; +КонецФункции diff --git a/tests/hub-address-test.os b/tests/hub-address-test.os new file mode 100644 index 0000000..854d284 --- /dev/null +++ b/tests/hub-address-test.os @@ -0,0 +1,201 @@ +#Использовать asserts +#Использовать "../src/core" + +Перем юТест; + +Функция ПолучитьСписокТестов(Знач Тестирование) Экспорт + + юТест = Тестирование; + + СписокТестов = Новый Массив; + + СписокТестов.Добавить("ТестДолжен_ОтличитьАдресОтИмениПакета"); + СписокТестов.Добавить("ТестДолжен_РазобратьАдресПакетаСПуломИСпецификацией"); + СписокТестов.Добавить("ТестДолжен_РазобратьАдресПакетаВПулеПоУмолчанию"); + СписокТестов.Добавить("ТестДолжен_ВыбратьСхемуПоУмолчанию"); + СписокТестов.Добавить("ТестДолжен_ОтказатьНаНеразобранномАдресе"); + СписокТестов.Добавить("ТестДолжен_СобратьСерверПулаПоАдресу"); + СписокТестов.Добавить("ТестДолжен_СобратьСерверВсехПуловПоАдресу"); + СписокТестов.Добавить("ТестДолжен_ВзятьАвторизациюАдресаИзНастроек"); + СписокТестов.Добавить("ТестДолжен_УстанавливатьПакетССервераИзАдреса"); + СписокТестов.Добавить("ТестДолжен_УстанавливатьПакетСХабаИзОпций"); + СписокТестов.Добавить("ТестДолжен_УстанавливатьПакетСоВсехПуловХаба"); + + Возврат СписокТестов; + +КонецФункции + +Процедура ПередЗапускомТеста() Экспорт + НастройкиOpm.СброситьНастройки(); +КонецПроцедуры + +Процедура ПослеЗапускаТеста() Экспорт + НастройкиOpm.СброситьНастройки(); +КонецПроцедуры + +Процедура ТестДолжен_ОтличитьАдресОтИмениПакета() Экспорт + + Для Каждого Адрес Из СтрРазделить("hub.example/pkg|https://hub|http://hub/corp/pkg|localhost/pkg|hub:3380/pkg", "|") Цикл + Ожидаем.Что(АдресаХаба.ЭтоАдресХаба(Адрес), Адрес).ЭтоИстина(); + КонецЦикла; + + Для Каждого Имя Из СтрРазделить("pkg|pkg@1.0.0|pkg@^1.4|hub.example|dir/pkg|./pkg|../dir/pkg|C:\dir\pkg", "|") Цикл + Ожидаем.Что(АдресаХаба.ЭтоАдресХаба(Имя), Имя).ЭтоЛожь(); + КонецЦикла; + +КонецПроцедуры + +Процедура ТестДолжен_РазобратьАдресПакетаСПуломИСпецификацией() Экспорт + + Адрес = АдресаХаба.АдресПакета("https://Hub.Example:8443/corp/pkg@stable"); + + Ожидаем.Что(Адрес.Сервер).Равно("https://hub.example"); + Ожидаем.Что(Адрес.Порт).Равно(8443); + Ожидаем.Что(Адрес.Пул).Равно("corp"); + Ожидаем.Что(Адрес.Пакет).Равно("pkg@stable"); + +КонецПроцедуры + +Процедура ТестДолжен_РазобратьАдресПакетаВПулеПоУмолчанию() Экспорт + + Адрес = АдресаХаба.АдресПакета("hub.example/pkg/"); + + Ожидаем.Что(Адрес.Пул).Равно(""); + Ожидаем.Что(Адрес.Пакет).Равно("pkg"); + Ожидаем.Что(Адрес.Порт).Равно(0); + +КонецПроцедуры + +Процедура ТестДолжен_ВыбратьСхемуПоУмолчанию() Экспорт + + Ожидаем.Что(АдресаХаба.АдресПакета("hub.example/pkg").Сервер, "публичное имя").Равно("https://hub.example"); + Ожидаем.Что(АдресаХаба.АдресПакета("localhost/pkg").Сервер, "localhost").Равно("http://localhost"); + Ожидаем.Что(АдресаХаба.АдресПакета("127.0.0.1/pkg").Сервер, "127.0.0.1").Равно("http://127.0.0.1"); + Ожидаем.Что(АдресаХаба.АдресПакета("hub.example:3380/pkg").Сервер, "явный порт").Равно("http://hub.example"); + Ожидаем.Что(АдресаХаба.Адрес("http://hub.example").Сервер, "явная схема").Равно("http://hub.example"); + +КонецПроцедуры + +Процедура ТестДолжен_ОтказатьНаНеразобранномАдресе() Экспорт + + Для Каждого Адрес Из СтрРазделить("hub.example|hub.example/a/b/c|hub.example:порт/pkg|ftp://hub/pkg", "|") Цикл + Ожидаем.Что(АдресаХаба).Метод("АдресПакета", ЗначенияПараметров(Адрес)) + .ВыбрасываетИсключение("ожидалось [схема://]хост[:порт]/[пул/]пакет[@спецификация]"); + КонецЦикла; + + Ожидаем.Что(АдресаХаба).Метод("Адрес", ЗначенияПараметров("hub.example/corp")) + .ВыбрасываетИсключение("ожидалось [схема://]хост[:порт] без пути, пул задаётся опцией --pool"); + + Адрес = АдресаХаба.Адрес("", "corp"); + Ожидаем.Что(Адрес.Сервер, "хаб по умолчанию").Равно("http://hub.oscript.io"); + Ожидаем.Что(Адрес.Пул).Равно("corp"); + +КонецПроцедуры + +Процедура ТестДолжен_СобратьСерверПулаПоАдресу() Экспорт + + Сервер = СерверыПакетов.ИзАдреса(АдресаХаба.Адрес("hub.example:3380", "corp")); + + Ожидаем.Что(Сервер.ПолучитьИмя()).Равно("http://hub.example:3380/corp"); + Ожидаем.Что(Сервер.Сервер).Равно("http://hub.example"); + Ожидаем.Что(Сервер.Порт).Равно(3380); + Ожидаем.Что(Сервер.ПутьНаСервере).Равно("/api/v1/pools/corp/download/"); + Ожидаем.Что(Сервер.РесурсПубликацииПакетов).Равно("/api/v1/pools/corp/push"); + + Сервер = СерверыПакетов.ИзАдреса(АдресаХаба.Адрес("hub.example")); + + Ожидаем.Что(Сервер.ПолучитьИмя()).Равно("https://hub.example"); + Ожидаем.Что(Сервер.ПутьНаСервере, "пул по умолчанию").Равно("/download/"); + Ожидаем.Что(Сервер.РесурсПубликацииПакетов, "пул по умолчанию").Равно("/push"); + +КонецПроцедуры + +Процедура ТестДолжен_СобратьСерверВсехПуловПоАдресу() Экспорт + + Сервер = СерверыПакетов.ИзАдреса(АдресаХаба.Адрес("hub.example:3380"), Истина); + + Ожидаем.Что(Сервер.ПолучитьИмя()).Равно("http://hub.example:3380/*"); + Ожидаем.Что(Сервер.ПутьНаСервере).Равно("/api/v1/download/"); + Ожидаем.Что(Сервер.АдресРесурса("pkg/pkg-stable.ospx", "stable")).Содержит("/api/v1/download/pkg/pkg-stable.ospx?spec=stable&engine="); + +КонецПроцедуры + +Процедура ТестДолжен_ВзятьАвторизациюАдресаИзНастроек() Экспорт + + НастройкиOpm.ДобавитьСерверПакетов(Новый СерверПакетов("чужой", "https://other.example", , , 0, 0, "Bearer чужой")); + НастройкиOpm.ДобавитьСерверПакетов(Новый СерверПакетов("свой", "https://HUB.example", , , 0, 0, "Bearer свой")); + + Ожидаем.Что(СерверыПакетов.ИзАдреса(АдресаХаба.АдресПакета("hub.example/corp/pkg")).Авторизация) + .Равно("Bearer свой"); + Ожидаем.Что(СерверыПакетов.ИзАдреса(АдресаХаба.АдресПакета("hub.example:8443/corp/pkg")).Авторизация, "другой порт") + .Равно(""); + +КонецПроцедуры + +// Порт 1 на своей машине не слушают, а заглушка не даёт подставить серверы по умолчанию: сеть не нужна. +Процедура ТестДолжен_УстанавливатьПакетССервераИзАдреса() Экспорт + + НастройкиOpm.ДобавитьСерверПакетов(ЗагрузитьСценарий(ОбъединитьПути(ТекущийСценарий().Каталог, + "fixtures", "ЗаглушкаСервераПакетов.os"))); + + Попытка + РаботаСПакетами.УстановитьПакетИзОблака("127.0.0.1:1/corp/pkg@1.0.0", РежимУстановкиПакетов.Локально, + ОбъединитьПути(юТест.ИмяВременногоФайла(), "oscript_modules")); + ВызватьИсключение "установка без хаба должна отказать"; + Исключение + Ожидаем.Что(ОписаниеОшибки()).Содержит("Ошибка установки пакета pkg"); + КонецПопытки; + + Сервер = НастройкиOpm.ПолучитьНастройки().СервераПакетов[1]; + Ожидаем.Что(Сервер.ПолучитьИмя()).Равно("http://127.0.0.1:1/corp"); + Ожидаем.Что(Сервер.ПутьНаСервере).Равно("/api/v1/pools/corp/download/"); + +КонецПроцедуры + +Процедура ТестДолжен_УстанавливатьПакетСХабаИзОпций() Экспорт + + НастройкиOpm.ДобавитьСерверПакетов(ЗагрузитьСценарий(ОбъединитьПути(ТекущийСценарий().Каталог, + "fixtures", "ЗаглушкаСервераПакетов.os"))); + + НастройкаУстановки = РаботаСПакетами.ПолучитьНастройкуУстановки(); + НастройкаУстановки.ИмяСервера = РаботаСПакетами.ПодключитьСерверХаба(АдресаХаба.Адрес("127.0.0.1:1", "corp")); + + Ожидаем.Что(НастройкаУстановки.ИмяСервера).Равно("http://127.0.0.1:1/corp"); + Ожидаем.Что(НастройкиOpm.ПолучитьНастройки().СервераПакетов[1].ПутьНаСервере).Равно("/api/v1/pools/corp/download/"); + + Попытка + РаботаСПакетами.УстановитьПакетИзОблака("pkg@1.0.0", РежимУстановкиПакетов.Локально, + ОбъединитьПути(юТест.ИмяВременногоФайла(), "oscript_modules"), НастройкаУстановки); + ВызватьИсключение "установка без хаба должна отказать"; + Исключение + Ожидаем.Что(ОписаниеОшибки()).Содержит("Ошибка установки пакета pkg"); + КонецПопытки; + +КонецПроцедуры + +Процедура ТестДолжен_УстанавливатьПакетСоВсехПуловХаба() Экспорт + + НастройкиOpm.ДобавитьСерверПакетов(ЗагрузитьСценарий(ОбъединитьПути(ТекущийСценарий().Каталог, + "fixtures", "ЗаглушкаСервераПакетов.os"))); + + НастройкаУстановки = РаботаСПакетами.ПолучитьНастройкуУстановки(); + НастройкаУстановки.ИмяСервера = РаботаСПакетами.ПодключитьСерверХаба(АдресаХаба.Адрес("127.0.0.1:1"), Истина); + + Ожидаем.Что(НастройкаУстановки.ИмяСервера).Равно("http://127.0.0.1:1/*"); + Ожидаем.Что(НастройкиOpm.ПолучитьНастройки().СервераПакетов[1].ПутьНаСервере).Равно("/api/v1/download/"); + + Попытка + РаботаСПакетами.УстановитьПакетИзОблака("pkg@1.0.0", РежимУстановкиПакетов.Локально, + ОбъединитьПути(юТест.ИмяВременногоФайла(), "oscript_modules"), НастройкаУстановки); + ВызватьИсключение "установка без хаба должна отказать"; + Исключение + Ожидаем.Что(ОписаниеОшибки()).Содержит("Ошибка установки пакета pkg"); + КонецПопытки; + +КонецПроцедуры + +Функция ЗначенияПараметров(Знач Значение) + Параметры = Новый Массив; + Параметры.Добавить(Значение); + Возврат Параметры; +КонецФункции diff --git a/tests/network-errors-test.os b/tests/network-errors-test.os new file mode 100644 index 0000000..3046487 --- /dev/null +++ b/tests/network-errors-test.os @@ -0,0 +1,95 @@ +#Использовать asserts +#Использовать "../src/core" + +Перем юТест; +Перем Хаб; + +Функция ПолучитьСписокТестов(Знач Тестирование) Экспорт + + юТест = Тестирование; + + СписокТестов = Новый Массив; + + СписокТестов.Добавить("ТестДолжен_СообщитьОтсутствиеФайлаПоКоду404"); + СписокТестов.Добавить("ТестДолжен_СообщитьОтказВДоступеПоКодам401И403"); + СписокТестов.Добавить("ТестДолжен_НазватьИнойКодОтвета"); + СписокТестов.Добавить("ТестДолжен_НазватьАдресСервераБезОтвета"); + + Возврат СписокТестов; + +КонецФункции + +Процедура ПередЗапускомТеста() Экспорт + + Хаб = ЗагрузитьЗаглушку("ЗаглушкаСервераПакетов.os"); + Хаб.ОбъявитьПакет("vrunner"); + + НастройкиOpm.СброситьНастройки(); + НастройкиOpm.ДобавитьСерверПакетов(Хаб); + +КонецПроцедуры + +Процедура ПослеЗапускаТеста() Экспорт + + НастройкиOpm.СброситьНастройки(); + ВременныеФайлы.Удалить(); + +КонецПроцедуры + +Процедура ТестДолжен_СообщитьОтсутствиеФайлаПоКоду404() Экспорт + + Ожидаем.Что(ТекстОтказа(404), "404") + .Равно("Ошибка установки пакета vrunner: файл vrunner.ospx в хабе не найден"); + +КонецПроцедуры + +Процедура ТестДолжен_СообщитьОтказВДоступеПоКодам401И403() Экспорт + + Ожидаем.Что(ТекстОтказа(401), "401") + .Равно("Ошибка установки пакета vrunner: хаб отказал в доступе к vrunner.ospx (код 401)"); + Ожидаем.Что(ТекстОтказа(403), "403") + .Равно("Ошибка установки пакета vrunner: хаб отказал в доступе к vrunner.ospx (код 403)"); + +КонецПроцедуры + +Процедура ТестДолжен_НазватьИнойКодОтвета() Экспорт + + Ожидаем.Что(ТекстОтказа(500), "500") + .Равно("Ошибка установки пакета vrunner: хаб ответил кодом 500 на запрос vrunner.ospx"); + +КонецПроцедуры + +Процедура ТестДолжен_НазватьАдресСервераБезОтвета() Экспорт + + // на порту 9 (discard) в тестовом окружении никто не слушает + Сервер = Новый СерверПакетов("закрытый", "http://127.0.0.1", "/", "/", 9); + + Ожидаем.Что(Сервер.ПолучитьРесурс("list.txt"), "ответа нет").Равно(Неопределено); + Ожидаем.Что(Сервер.ПоследняяОшибкаСети, "сообщение с адресом") + .Равно("Не удалось подключиться к http://127.0.0.1:9"); + + // недоступный сервер при получении списка пакетов только предупреждает + Сервер.ПолучитьПакеты(); + +КонецПроцедуры + +Функция ТекстОтказа(Знач Код) + + Ответ = ЗагрузитьЗаглушку("ЗаглушкаОтветаХаба.os"); + Ответ.КодСостояния = Код; + Хаб.Отвечать(Ответ); + + Попытка + Менеджер = Новый МенеджерПолученияПакетов(); + Менеджер.ПолучитьПакет("vrunner", Неопределено); + Исключение + Возврат ИнформацияОбОшибке().Описание; + КонецПопытки; + + ВызватьИсключение СтрШаблон("Скачивание с кодом %1 должно было провалиться", Код); + +КонецФункции + +Функция ЗагрузитьЗаглушку(Знач ИмяФайла) + Возврат ЗагрузитьСценарий(ОбъединитьПути(ТекущийСценарий().Каталог, "fixtures", ИмяФайла)); +КонецФункции diff --git a/tests/package-spec-test.os b/tests/package-spec-test.os new file mode 100644 index 0000000..8e5b047 --- /dev/null +++ b/tests/package-spec-test.os @@ -0,0 +1,218 @@ +#Использовать asserts +#Использовать "../src/core" + +Перем юТест; +Перем Хаб; + +Функция ПолучитьСписокТестов(Знач Тестирование) Экспорт + + юТест = Тестирование; + + СписокТестов = Новый Массив; + + СписокТестов.Добавить("ТестДолжен_ЗапроситьФайлВерсииИСпецификациюОтдельно"); + СписокТестов.Добавить("ТестДолжен_ЗапроситьФайлБезСпецификацииКогдаЕёНеНазвали"); + СписокТестов.Добавить("ТестДолжен_СобратьАдресЗапросаСоСпецификацией"); + СписокТестов.Добавить("ТестДолжен_СобратьАдресЗапросаБезСпецификации"); + СписокТестов.Добавить("ТестДолжен_ЗакодироватьСпецификациюВАдресе"); + СписокТестов.Добавить("ТестДолжен_ПередатьВерсиюДвижкаТолькоВЗапросеФайлаПакета"); + СписокТестов.Добавить("ТестДолжен_ПоказатьПричинуОтказаИзОтветаХаба"); + СписокТестов.Добавить("ТестДолжен_СохранитьПрежнийОтказНаТочнуюВерсию"); + СписокТестов.Добавить("ТестДолжен_ЗапретитьФайловомуСерверуПодборПодСпецификацию"); + СписокТестов.Добавить("ТестДолжен_ОставитьФайловомуСерверуПодборБезСпецификации"); + + Возврат СписокТестов; + +КонецФункции + +Процедура ПередЗапускомТеста() Экспорт + + Хаб = ЗагрузитьЗаглушку("ЗаглушкаСервераПакетов.os"); + Хаб.ОбъявитьПакет("vrunner"); + Хаб.Отвечать(ОтветХаба(200)); + + НастройкиOpm.СброситьНастройки(); + НастройкиOpm.ДобавитьСерверПакетов(Хаб); + +КонецПроцедуры + +Процедура ПослеЗапускаТеста() Экспорт + + НастройкиOpm.СброситьНастройки(); + ВременныеФайлы.Удалить(); + +КонецПроцедуры + +Процедура ТестДолжен_ЗапроситьФайлВерсииИСпецификациюОтдельно() Экспорт + + Для Каждого Спецификация Из СтрРазделить("stable,^1.4,~1.4,1.4", ",") Цикл + + Скачать("vrunner", Спецификация); + + Ожидаем.Что(Хаб.ПоследнийЗапрос().ИмяРесурса, "имя ресурса для " + Спецификация) + .Равно(СтрШаблон("vrunner/vrunner-%1.ospx", Спецификация)); + Ожидаем.Что(Хаб.ПоследнийЗапрос().Спецификация, "спецификация").Равно(Спецификация); + + КонецЦикла; + +КонецПроцедуры + +Процедура ТестДолжен_ЗапроситьФайлБезСпецификацииКогдаЕёНеНазвали() Экспорт + + Скачать("vrunner", Неопределено); + + Ожидаем.Что(Хаб.ПоследнийЗапрос().ИмяРесурса, "имя ресурса").Равно("vrunner/vrunner.ospx"); + Ожидаем.Что(Хаб.ПоследнийЗапрос().Спецификация, "спецификация").Равно(Неопределено); + +КонецПроцедуры + +Процедура ТестДолжен_СобратьАдресЗапросаСоСпецификацией() Экспорт + + Сервер = СерверПробы(); + + Ожидаем.Что(Сервер.АдресРесурса("vrunner/vrunner-stable.ospx", "stable"), "тег") + .Равно("/download/vrunner/vrunner-stable.ospx?spec=stable&" + Движок()); + Ожидаем.Что(Сервер.АдресРесурса("vrunner/vrunner-1.0.0.ospx", "1.0.0"), "точная версия") + .Равно("/download/vrunner/vrunner-1.0.0.ospx?spec=1.0.0&" + Движок()); + +КонецПроцедуры + +Процедура ТестДолжен_СобратьАдресЗапросаБезСпецификации() Экспорт + + Сервер = СерверПробы(); + + Ожидаем.Что(Сервер.АдресРесурса("vrunner/vrunner.ospx"), "файл пакета").Равно("/download/vrunner/vrunner.ospx?" + Движок()); + Ожидаем.Что(Сервер.АдресРесурса("vrunner/vrunner-.ospx", ""), "пустая спецификация") + .Равно("/download/vrunner/vrunner-.ospx?" + Движок()); + Ожидаем.Что(Сервер.АдресРесурса("list.txt"), "список пакетов").Равно("/download/list.txt"); + +КонецПроцедуры + +Процедура ТестДолжен_ЗакодироватьСпецификациюВАдресе() Экспорт + + Сервер = СерверПробы(); + + Для Каждого Образец Из СтрРазделить("^1.4|~1.4|1.0.0-rc.1+build|>=1.0 <2.0|feature/foo", "|") Цикл + + Адрес = Сервер.АдресРесурса("vrunner/vrunner.ospx", Образец); + Параметр = СтрРазделить(Сред(Адрес, СтрНайти(Адрес, "?spec=") + СтрДлина("?spec=")), "&")[0]; + + Ожидаем.Что(СтрНайти(Параметр, "+") + СтрНайти(Параметр, " ") + СтрНайти(Параметр, "/"), + "служебные символы закодированы в " + Адрес).Равно(0); + Ожидаем.Что(РаскодироватьСтроку(Параметр, СпособКодированияСтроки.КодировкаURL), "значение в " + Адрес) + .Равно(Образец); + + КонецЦикла; + + Ожидаем.Что(Сервер.АдресРесурса("vrunner/vrunner-^1.4.ospx", "^1.4"), "имя файла") + .Равно("/download/vrunner/vrunner-%5E1.4.ospx?spec=%5E1.4&" + Движок()); + +КонецПроцедуры + +Процедура ТестДолжен_ПередатьВерсиюДвижкаТолькоВЗапросеФайлаПакета() Экспорт + + Сервер = СерверПробы(); + + Ожидаем.Что(Сервер.АдресРесурса("vrunner/vrunner-stable.ospx", "stable"), "файл по тегу") + .Содержит("&" + Движок()); + Ожидаем.Что(Сервер.АдресРесурса("vrunner/vrunner.ospx"), "файл без спецификации") + .Содержит("?" + Движок()); + Ожидаем.Что(Сервер.АдресРесурса("list.txt"), "список пакетов").Не_().Содержит("engine="); + +КонецПроцедуры + +Процедура ТестДолжен_ПоказатьПричинуОтказаИзОтветаХаба() Экспорт + + Хаб.Отвечать(ОтветХаба(404, " нет версии под движок 1.9.0" + Символы.ПС)); + Ожидаем.Что(ТекстОтказа("vrunner", "stable"), "404 с причиной") + .Содержит("Ошибка установки пакета vrunner: нет версии под движок 1.9.0"); + + Хаб.Отвечать(ОтветХаба(422, "bad request: engine")); + Ожидаем.Что(ТекстОтказа("vrunner", Неопределено), "422").Содержит("Ошибка установки пакета vrunner: bad request: engine"); + + Длинное = СтрСоединить(Новый Массив(400), "x") + "конец"; + Хаб.Отвечать(ОтветХаба(404, Длинное)); + Ожидаем.Что(ТекстОтказа("vrunner", Неопределено), "длинный ответ").Не_().Содержит("конец"); + +КонецПроцедуры + +Процедура ТестДолжен_СохранитьПрежнийОтказНаТочнуюВерсию() Экспорт + + Хаб.Отвечать(ОтветХаба(404)); + + Ожидаем.Что(ТекстОтказа("vrunner", "1.4"), "точная версия").Содержит("файл vrunner-1.4.ospx в хабе не найден"); + Ожидаем.Что(ТекстОтказа("vrunner", Неопределено), "без спецификации") + .Содержит("файл vrunner.ospx в хабе не найден"); + Ожидаем.Что(ТекстОтказа("vrunner", ""), "пустая спецификация").Содержит("файл vrunner-.ospx в хабе не найден"); + +КонецПроцедуры + +Процедура ТестДолжен_ЗапретитьФайловомуСерверуПодборПодСпецификацию() Экспорт + + Сервер = Новый ФайловыйСерверПакетов("зеркалоПробы", "file://", КаталогФайловогоСервера()); + + Ожидаем.Что(Сервер.ПолучитьРесурс("vrunner/vrunner-stable.ospx", "stable").КодСостояния, "тег").Равно(404); + Ожидаем.Что(Сервер.ПолучитьРесурс("vrunner/vrunner-0.0.1.ospx", "0.0.1").КодСостояния, "версии нет") + .Равно(404); + Ожидаем.Что(Сервер.ПолучитьРесурс("vrunner/vrunner-9.9.9.ospx", "9.9.9").КодСостояния, "версия есть") + .Равно(200); + +КонецПроцедуры + +Процедура ТестДолжен_ОставитьФайловомуСерверуПодборБезСпецификации() Экспорт + + Сервер = Новый ФайловыйСерверПакетов("зеркалоПробы", "file://", КаталогФайловогоСервера()); + + Ожидаем.Что(Сервер.ПолучитьРесурс("vrunner/vrunner.ospx").КодСостояния, "подбор старшей версии").Равно(200); + +КонецПроцедуры + +Функция ЗагрузитьЗаглушку(Знач ИмяФайла) + Возврат ЗагрузитьСценарий(ОбъединитьПути(ТекущийСценарий().Каталог, "fixtures", ИмяФайла)); +КонецФункции + +Функция ОтветХаба(Знач Код, Знач Тело = "") + Ответ = ЗагрузитьЗаглушку("ЗаглушкаОтветаХаба.os"); + Ответ.КодСостояния = Код; + Ответ.Тело = Тело; + Возврат Ответ; +КонецФункции + +// Версия движка в строке запроса закодирована: плюс сборки иначе читается пробелом. +Функция Движок() + Возврат "engine=" + КодироватьСтроку(Новый СистемнаяИнформация().Версия, СпособКодированияСтроки.КодировкаURL); +КонецФункции + +Функция СерверПробы() + Возврат Новый СерверПакетов("ХабПробы", "http://hub.example.com", "/download/", "/push", 80); +КонецФункции + +Функция Скачать(Знач ИмяПакета, Знач Спецификация) + Возврат Новый МенеджерПолученияПакетов().ПолучитьПакет(ИмяПакета, Спецификация); +КонецФункции + +Функция ТекстОтказа(Знач ИмяПакета, Знач Спецификация) + + Попытка + Скачать(ИмяПакета, Спецификация); + Исключение + Возврат ОписаниеОшибки(); + КонецПопытки; + + ВызватьИсключение СтрШаблон("Скачивание <%1@%2> должно было провалиться", ИмяПакета, Спецификация); + +КонецФункции + +// В каталоге одна версия 9.9.9: подбор выдал бы её за любую запрошенную. +Функция КаталогФайловогоСервера() + + Каталог = юТест.ИмяВременногоФайла(); + СоздатьКаталог(ОбъединитьПути(Каталог, "vrunner")); + + Запись = Новый ЗаписьТекста(ОбъединитьПути(Каталог, "vrunner", "vrunner-9.9.9.ospx")); + Запись.Записать("файл пакета"); + Запись.Закрыть(); + + Возврат Каталог; + +КонецФункции diff --git a/tests/push-target-test.os b/tests/push-target-test.os new file mode 100644 index 0000000..a1cc00e --- /dev/null +++ b/tests/push-target-test.os @@ -0,0 +1,92 @@ +#Использовать asserts +#Использовать "../src/core" + +Функция ПолучитьСписокТестов(Знач Тестирование) Экспорт + + СписокТестов = Новый Массив; + + СписокТестов.Добавить("ТестДолжен_ПубликоватьВПулИзОпцииХаба"); + СписокТестов.Добавить("ТестДолжен_ОтказатьНаПутиВОпцииХаба"); + СписокТестов.Добавить("ТестДолжен_ПубликоватьВПулХабаПоУмолчанию"); + СписокТестов.Добавить("ТестДолжен_ПубликоватьНаСерверИзНастроекПоИмени"); + СписокТестов.Добавить("ТестДолжен_ПубликоватьВХабПоУмолчанию"); + СписокТестов.Добавить("ТестДолжен_ДобавитьForceКРесурсуПубликации"); + СписокТестов.Добавить("ТестДолжен_НеДобавлятьForceБезОпции"); + + Возврат СписокТестов; + +КонецФункции + +Процедура ПередЗапускомТеста() Экспорт + НастройкиOpm.СброситьНастройки(); +КонецПроцедуры + +Процедура ПослеЗапускаТеста() Экспорт + НастройкиOpm.СброситьНастройки(); +КонецПроцедуры + +Процедура ТестДолжен_ПубликоватьВПулИзОпцииХаба() Экспорт + + Сервер = СерверыПакетов.ДляПубликации("127.0.0.1:3380", "", "corp"); + + Ожидаем.Что(Сервер.Сервер).Равно("http://127.0.0.1"); + Ожидаем.Что(Сервер.Порт).Равно(3380); + Ожидаем.Что(Сервер.РесурсПубликацииПакетов).Равно("/api/v1/pools/corp/push"); + +КонецПроцедуры + +Процедура ТестДолжен_ОтказатьНаПутиВОпцииХаба() Экспорт + + Параметры = Новый Массив; + Параметры.Добавить("hub.example/corp"); + Параметры.Добавить(""); + + Ожидаем.Что(СерверыПакетов).Метод("ДляПубликации", Параметры).ВыбрасываетИсключение("без пути, пул задаётся опцией --pool"); + +КонецПроцедуры + +Процедура ТестДолжен_ПубликоватьВПулХабаПоУмолчанию() Экспорт + + Сервер = СерверыПакетов.ДляПубликации("", "", "corp"); + + Ожидаем.Что(Сервер.Сервер).Равно("http://hub.oscript.io"); + Ожидаем.Что(Сервер.РесурсПубликацииПакетов).Равно("/api/v1/pools/corp/push"); + +КонецПроцедуры + +Процедура ТестДолжен_ПубликоватьНаСерверИзНастроекПоИмени() Экспорт + + НастройкиOpm.ДобавитьСерверПакетов(Новый СерверПакетов("corp", "https://hub.example", , "/api/v1/pools/corp/push")); + + Ожидаем.Что(СерверыПакетов.ДляПубликации("", "corp").РесурсПубликацииПакетов).Равно("/api/v1/pools/corp/push"); + +КонецПроцедуры + +Процедура ТестДолжен_ПубликоватьВХабПоУмолчанию() Экспорт + + Сервер = СерверыПакетов.ДляПубликации("", ""); + + Ожидаем.Что(Сервер.Сервер).Равно(КонстантыOpm.СерверУдаленногоХранилища); + Ожидаем.Что(Сервер.РесурсПубликацииПакетов).Равно(КонстантыOpm.РесурсПубликацииПакетов); + +КонецПроцедуры + +Процедура ТестДолжен_ДобавитьForceКРесурсуПубликации() Экспорт + + Сервер = СерверыПакетов.ДляПубликации("127.0.0.1:3380", "", "corp", Истина); + Ожидаем.Что(Сервер.РесурсПубликацииПакетов, "пул хаба").Равно("/api/v1/pools/corp/push?force=1"); + + НастройкиOpm.ДобавитьСерверПакетов(Новый СерверПакетов("corp", "https://hub.example", , "/push?owner=corp")); + Сервер = СерверыПакетов.ДляПубликации("", "corp", , Истина); + Ожидаем.Что(Сервер.РесурсПубликацииПакетов, "ресурс с параметрами").Равно("/push?owner=corp&force=1"); + +КонецПроцедуры + +Процедура ТестДолжен_НеДобавлятьForceБезОпции() Экспорт + + Ожидаем.Что(СерверыПакетов.ДляПубликации("127.0.0.1:3380", "", "corp").РесурсПубликацииПакетов) + .Равно("/api/v1/pools/corp/push"); + Ожидаем.Что(СерверыПакетов.ДляПубликации("127.0.0.1:3380", "", "corp", Ложь).РесурсПубликацииПакетов) + .Равно("/api/v1/pools/corp/push"); + +КонецПроцедуры diff --git a/tests/trusted-publish-test.os b/tests/trusted-publish-test.os new file mode 100644 index 0000000..62dbd3f --- /dev/null +++ b/tests/trusted-publish-test.os @@ -0,0 +1,95 @@ +#Использовать asserts +#Использовать "../src/core" + +Перем юТест; + +Функция ПолучитьСписокТестов(Знач Тестирование) Экспорт + + юТест = Тестирование; + + СписокТестов = Новый Массив; + + СписокТестов.Добавить("ТестДолжен_ВзятьТокенИзПеременнойGitLab"); + СписокТестов.Добавить("ТестДолжен_НазватьПеременнуюGitLabВОтказе"); + СписокТестов.Добавить("ТестДолжен_ЗапроситьТокенУСервисаGitHub"); + СписокТестов.Добавить("ТестДолжен_ЗапроситьТокенСАдресомХабаВАудитории"); + СписокТестов.Добавить("ТестДолжен_ОтказатьБезПеременныхКонвейера"); + + Возврат СписокТестов; + +КонецФункции + +Процедура ПослеЗапускаТеста() Экспорт + УстановитьПеременнуюСреды("OPENHUB_TOKEN", ""); + УстановитьПеременнуюСреды("ACTIONS_ID_TOKEN_REQUEST_URL", ""); + УстановитьПеременнуюСреды("ACTIONS_ID_TOKEN_REQUEST_TOKEN", ""); +КонецПроцедуры + +Процедура ТестДолжен_ВзятьТокенИзПеременнойGitLab() Экспорт + + УстановитьПеременнуюСреды("OPENHUB_TOKEN", "id.token.gitlab"); + + Ожидаем.Что(ТокенКонвейераCI.Получить(СерверХаба())).Равно("id.token.gitlab"); + +КонецПроцедуры + +Процедура ТестДолжен_НазватьПеременнуюGitLabВОтказе() Экспорт + + Параметры = Новый Массив; + Параметры.Добавить(СерверХаба()); + + Ожидаем.Что(ТокенКонвейераCI).Метод("Получить", Параметры).ВыбрасываетИсключение("OPENHUB_TOKEN в блоке id_tokens"); + +КонецПроцедуры + +Процедура ТестДолжен_ЗапроситьТокенУСервисаGitHub() Экспорт + + Соединение = СоединениеGitHub(); + + Ожидаем.Что(ТокенКонвейераCI.Получить(СерверХаба(), "https://aud.example", Соединение)).Равно("id.token.github"); + Ожидаем.Что(Соединение.Запрос.АдресРесурса).Равно("/idtoken?api-version=2.0&audience=https%3A%2F%2Faud.example"); + Ожидаем.Что(Соединение.Запрос.Заголовки["Authorization"]).Равно("bearer токен-запроса"); + +КонецПроцедуры + +Процедура ТестДолжен_ЗапроситьТокенСАдресомХабаВАудитории() Экспорт + + Соединение = СоединениеGitHub(); + + ТокенКонвейераCI.Получить(СерверХаба(), "", Соединение); + + Ожидаем.Что(Соединение.Запрос.АдресРесурса).Равно("/idtoken?api-version=2.0&audience=https%3A%2F%2Fhub.example"); + +КонецПроцедуры + +Процедура ТестДолжен_ОтказатьБезПеременныхКонвейера() Экспорт + + Параметры = Новый Массив; + Параметры.Добавить(СерверХаба()); + + Ожидаем.Что(ТокенКонвейераCI).Метод("Получить", Параметры).ВыбрасываетИсключение("permissions: id-token: write"); + +КонецПроцедуры + +Функция СерверХаба() + Возврат СерверыПакетов.ИзАдреса(АдресаХаба.Адрес("hub.example", "corp")); +КонецФункции + +Функция СоединениеGitHub() + + УстановитьПеременнуюСреды("ACTIONS_ID_TOKEN_REQUEST_URL", "https://tokens.example/idtoken?api-version=2.0"); + УстановитьПеременнуюСреды("ACTIONS_ID_TOKEN_REQUEST_TOKEN", "токен-запроса"); + + Ответ = ЗагрузитьЗаглушку("ЗаглушкаОтветаХаба.os"); + Ответ.Тело = "{""count"": 1, ""value"": ""id.token.github""}"; + + Соединение = ЗагрузитьЗаглушку("ЗаглушкаКонвейера.os"); + Соединение.Ответ = Ответ; + + Возврат Соединение; + +КонецФункции + +Функция ЗагрузитьЗаглушку(Знач ИмяФайла) + Возврат ЗагрузитьСценарий(ОбъединитьПути(ТекущийСценарий().Каталог, "fixtures", ИмяФайла)); +КонецФункции