Compare commits
6 Commits
b6534f809b
...
16c3f2057b
| Author | SHA1 | Date | |
|---|---|---|---|
| 16c3f2057b | |||
| 62fe2ab536 | |||
| 9be207154e | |||
| f431fc3c1c | |||
| be337c0d40 | |||
| 0a0ae1f1e6 |
+1
-1
@@ -10,7 +10,7 @@ RUN apt-get update \
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY pyproject.toml README.md ./
|
COPY pyproject.toml README.md VERSION ./
|
||||||
COPY app ./app
|
COPY app ./app
|
||||||
COPY alembic ./alembic
|
COPY alembic ./alembic
|
||||||
COPY alembic.ini ./
|
COPY alembic.ini ./
|
||||||
|
|||||||
Vendored
+92
-62
@@ -1,24 +1,22 @@
|
|||||||
// Wrapped — CI: multi-arch (amd64+arm64) build & push to Harbor + Docker Hub
|
// Wrapped — CI: multi-arch push (Harbor + Docker Hub), затем асинхронный Deploy
|
||||||
//
|
//
|
||||||
// Триггер: push в main (webhook Gitea → Jenkins Multibranch / Pipeline from SCM).
|
// Ресурсы k3s: Builder и Deploy не должны держать два DinD-пода сразу.
|
||||||
// После успешного Build & push → trigger job DEPLOY_JOB (Jenkinsfile.deploy).
|
// 1) стадии сборки на agent label 'docker' (DinD)
|
||||||
|
// 2) при успехе — trigger Deploy с wait:false и agent none → Builder-под умирает
|
||||||
|
// 3) Deploy стартует отдельно уже без Builder
|
||||||
//
|
//
|
||||||
// Credentials в Jenkins (Manage Credentials → System → Global):
|
// Версия образа = VERSION из коммита. Bump: make bump-patch / make push.
|
||||||
// harbor-devops-tools-push-pull-access — Username with password (Harbor devops-tools)
|
|
||||||
// docker-hub — Username with password (Docker Hub)
|
|
||||||
//
|
//
|
||||||
// Agent: cloud kubernetes, pod template default (labels docker / dind)
|
// Credentials (Global):
|
||||||
|
// harbor-devops-tools-push-pull-access — Harbor devops-tools (robot)
|
||||||
|
// docker-hub — Docker Hub (inecs)
|
||||||
|
// ssh-gitea-key — SCM checkout Gitea
|
||||||
|
// k3s-kubeconfig — в job Deploy (Jenkinsfile.deploy)
|
||||||
//
|
//
|
||||||
// Образы:
|
// Deploy job: devops-tools/wrapped/wrapped-deploy/main
|
||||||
// hub.antropoff.ru/devops-tools/wrapped:<sha|0.1.0|latest>
|
|
||||||
// inecs/wrapped:<sha|0.1.0|latest>
|
|
||||||
//
|
|
||||||
// Связка Build → Deploy:
|
|
||||||
// Multibranch Deploy job, Script Path = Jenkinsfile.deploy
|
|
||||||
// путь по умолчанию: «Devops Tools/Wrapped Deploy/main» (поменяй DEPLOY_JOB при другом имени)
|
|
||||||
|
|
||||||
pipeline {
|
pipeline {
|
||||||
agent { label 'docker' }
|
agent none
|
||||||
|
|
||||||
options {
|
options {
|
||||||
buildDiscarder(logRotator(numToKeepStr: '20'))
|
buildDiscarder(logRotator(numToKeepStr: '20'))
|
||||||
@@ -31,14 +29,21 @@ pipeline {
|
|||||||
HARBOR_REGISTRY = 'hub.antropoff.ru'
|
HARBOR_REGISTRY = 'hub.antropoff.ru'
|
||||||
HARBOR_IMAGE = 'hub.antropoff.ru/devops-tools/wrapped'
|
HARBOR_IMAGE = 'hub.antropoff.ru/devops-tools/wrapped'
|
||||||
DOCKERHUB_IMAGE = 'inecs/wrapped'
|
DOCKERHUB_IMAGE = 'inecs/wrapped'
|
||||||
IMAGE_VERSION = '0.1.0'
|
|
||||||
RELEASE_TAG = "${env.GIT_COMMIT?.take(7) ?: 'dev'}"
|
RELEASE_TAG = "${env.GIT_COMMIT?.take(7) ?: 'dev'}"
|
||||||
BUILDX_BUILDER = "jenkins-wrapped-${env.BUILD_NUMBER}"
|
BUILDX_BUILDER = "jenkins-wrapped-${env.BUILD_NUMBER}"
|
||||||
// Multibranch: Folder/Job/branch — как в UI «Devops Tools » Wrapped Deploy » main»
|
DEPLOY_JOB = 'devops-tools/wrapped/wrapped-deploy/main'
|
||||||
DEPLOY_JOB = 'Devops Tools/Wrapped Deploy/main'
|
|
||||||
TZ = 'Europe/Moscow'
|
TZ = 'Europe/Moscow'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('Build image') {
|
||||||
|
when {
|
||||||
|
anyOf {
|
||||||
|
branch 'main'
|
||||||
|
branch 'master'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
agent { label 'docker' }
|
||||||
stages {
|
stages {
|
||||||
stage('Checkout') {
|
stage('Checkout') {
|
||||||
steps {
|
steps {
|
||||||
@@ -46,13 +51,24 @@ pipeline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stage('Version') {
|
||||||
|
steps {
|
||||||
|
container('docker') {
|
||||||
|
script {
|
||||||
|
env.IMAGE_VERSION = sh(
|
||||||
|
script: "tr -d '[:space:]' < VERSION",
|
||||||
|
returnStdout: true
|
||||||
|
).trim()
|
||||||
|
if (!env.IMAGE_VERSION) {
|
||||||
|
error('VERSION file is empty')
|
||||||
|
}
|
||||||
|
echo "IMAGE_VERSION from VERSION → ${env.IMAGE_VERSION}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
stage('Build & push') {
|
stage('Build & push') {
|
||||||
when {
|
|
||||||
anyOf {
|
|
||||||
branch 'main'
|
|
||||||
branch 'master'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
steps {
|
steps {
|
||||||
withCredentials([
|
withCredentials([
|
||||||
usernamePassword(
|
usernamePassword(
|
||||||
@@ -70,6 +86,9 @@ pipeline {
|
|||||||
sh '''
|
sh '''
|
||||||
set -eux
|
set -eux
|
||||||
|
|
||||||
|
test -n "${IMAGE_VERSION}"
|
||||||
|
echo "Building tags: ${IMAGE_VERSION}, ${RELEASE_TAG}, latest"
|
||||||
|
|
||||||
echo "$HARBOR_PASS" | docker login "$HARBOR_REGISTRY" -u "$HARBOR_USER" --password-stdin
|
echo "$HARBOR_PASS" | docker login "$HARBOR_REGISTRY" -u "$HARBOR_USER" --password-stdin
|
||||||
echo "$DOCKERHUB_PASS" | docker login -u "$DOCKERHUB_USER" --password-stdin
|
echo "$DOCKERHUB_PASS" | docker login -u "$DOCKERHUB_USER" --password-stdin
|
||||||
|
|
||||||
@@ -77,7 +96,6 @@ pipeline {
|
|||||||
docker buildx create --name "$BUILDX_BUILDER" --driver docker-container --use
|
docker buildx create --name "$BUILDX_BUILDER" --driver docker-container --use
|
||||||
docker buildx inspect --bootstrap >/dev/null
|
docker buildx inspect --bootstrap >/dev/null
|
||||||
|
|
||||||
# Per-arch images → both registries (Harbor is happier than a single multi-arch --push)
|
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--platform linux/amd64 \
|
--platform linux/amd64 \
|
||||||
--provenance=false --sbom=false --push \
|
--provenance=false --sbom=false --push \
|
||||||
@@ -92,7 +110,6 @@ pipeline {
|
|||||||
-t "${DOCKERHUB_IMAGE}:${RELEASE_TAG}-arm64" \
|
-t "${DOCKERHUB_IMAGE}:${RELEASE_TAG}-arm64" \
|
||||||
-f Dockerfile .
|
-f Dockerfile .
|
||||||
|
|
||||||
# Multi-arch manifests: :sha, :0.1.0, :latest on each registry
|
|
||||||
for IMAGE in "$HARBOR_IMAGE" "$DOCKERHUB_IMAGE"; do
|
for IMAGE in "$HARBOR_IMAGE" "$DOCKERHUB_IMAGE"; do
|
||||||
docker buildx imagetools create \
|
docker buildx imagetools create \
|
||||||
-t "${IMAGE}:${RELEASE_TAG}" \
|
-t "${IMAGE}:${RELEASE_TAG}" \
|
||||||
@@ -103,9 +120,9 @@ pipeline {
|
|||||||
done
|
done
|
||||||
|
|
||||||
echo "--- Harbor ---"
|
echo "--- Harbor ---"
|
||||||
docker buildx imagetools inspect "${HARBOR_IMAGE}:${RELEASE_TAG}" | sed -n '1,40p'
|
docker buildx imagetools inspect "${HARBOR_IMAGE}:${IMAGE_VERSION}" | sed -n '1,40p'
|
||||||
echo "--- Docker Hub ---"
|
echo "--- Docker Hub ---"
|
||||||
docker buildx imagetools inspect "${DOCKERHUB_IMAGE}:${RELEASE_TAG}" | sed -n '1,40p'
|
docker buildx imagetools inspect "${DOCKERHUB_IMAGE}:${IMAGE_VERSION}" | sed -n '1,40p'
|
||||||
|
|
||||||
docker buildx rm "$BUILDX_BUILDER" || true
|
docker buildx rm "$BUILDX_BUILDER" || true
|
||||||
'''
|
'''
|
||||||
@@ -113,41 +130,8 @@ pipeline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('Trigger deploy') {
|
|
||||||
when {
|
|
||||||
anyOf {
|
|
||||||
branch 'main'
|
|
||||||
branch 'master'
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
steps {
|
|
||||||
script {
|
|
||||||
echo "Triggering ${DEPLOY_JOB} → ${DOCKERHUB_IMAGE}:${IMAGE_VERSION}"
|
|
||||||
build(
|
|
||||||
job: env.DEPLOY_JOB,
|
|
||||||
wait: true,
|
|
||||||
propagate: true,
|
|
||||||
parameters: [
|
|
||||||
string(name: 'IMAGE_REPOSITORY', value: env.DOCKERHUB_IMAGE),
|
|
||||||
string(name: 'IMAGE_TAG', value: env.IMAGE_VERSION),
|
|
||||||
string(name: 'IMAGE_PULL_POLICY', value: 'Always'),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
post {
|
post {
|
||||||
success {
|
|
||||||
echo "✓ Harbor: ${HARBOR_IMAGE}:{${RELEASE_TAG},${IMAGE_VERSION},latest}"
|
|
||||||
echo "✓ Docker Hub: ${DOCKERHUB_IMAGE}:{${RELEASE_TAG},${IMAGE_VERSION},latest}"
|
|
||||||
echo "✓ Deploy: ${DEPLOY_JOB} (${DOCKERHUB_IMAGE}:${IMAGE_VERSION})"
|
|
||||||
}
|
|
||||||
failure {
|
|
||||||
echo "✗ Сборка, push или trigger deploy не удались — см. лог стадий"
|
|
||||||
}
|
|
||||||
always {
|
always {
|
||||||
script {
|
script {
|
||||||
try {
|
try {
|
||||||
@@ -155,9 +139,55 @@ pipeline {
|
|||||||
sh 'docker buildx rm "$BUILDX_BUILDER" 2>/dev/null || true'
|
sh 'docker buildx rm "$BUILDX_BUILDER" 2>/dev/null || true'
|
||||||
}
|
}
|
||||||
} catch (Ignored) {
|
} catch (Ignored) {
|
||||||
// agent may already be gone
|
// pod may already be gone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
success {
|
||||||
|
echo "✓ Build OK: ${DOCKERHUB_IMAGE}:{${RELEASE_TAG},${IMAGE_VERSION},latest}"
|
||||||
|
}
|
||||||
|
failure {
|
||||||
|
echo "✗ Build/push не удались — Deploy не запускается"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Без DinD: только очередь Deploy, Builder-под уже освобождён
|
||||||
|
stage('Trigger deploy') {
|
||||||
|
when {
|
||||||
|
anyOf {
|
||||||
|
branch 'main'
|
||||||
|
branch 'master'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
agent none
|
||||||
|
steps {
|
||||||
|
script {
|
||||||
|
echo "Build finished — triggering ${DEPLOY_JOB} → ${DOCKERHUB_IMAGE}:${IMAGE_VERSION} (wait: false)"
|
||||||
|
build(
|
||||||
|
job: env.DEPLOY_JOB,
|
||||||
|
wait: false,
|
||||||
|
propagate: false,
|
||||||
|
parameters: [
|
||||||
|
string(name: 'IMAGE_REPOSITORY', value: env.DOCKERHUB_IMAGE),
|
||||||
|
string(name: 'IMAGE_TAG', value: env.IMAGE_VERSION),
|
||||||
|
string(name: 'IMAGE_PULL_POLICY', value: 'Always'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
echo "Deploy queued; Builder pipeline exits and releases cluster resources."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
success {
|
||||||
|
echo "✓ Version: ${IMAGE_VERSION}"
|
||||||
|
echo "✓ Image: ${DOCKERHUB_IMAGE}:{${RELEASE_TAG},${IMAGE_VERSION},latest}"
|
||||||
|
echo "✓ Deploy: ${DEPLOY_JOB} triggered (async)"
|
||||||
|
}
|
||||||
|
failure {
|
||||||
|
echo "✗ Сборка не удалась — см. лог Build image"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-5
@@ -7,8 +7,8 @@
|
|||||||
// Job: отдельный Multibranch Pipeline, Script Path = Jenkinsfile.deploy
|
// Job: отдельный Multibranch Pipeline, Script Path = Jenkinsfile.deploy
|
||||||
// Имя по умолчанию (см. DEPLOY_JOB в Jenkinsfile): «Devops Tools/Wrapped Deploy»
|
// Имя по умолчанию (см. DEPLOY_JOB в Jenkinsfile): «Devops Tools/Wrapped Deploy»
|
||||||
// Триггер: вручную (Build with Parameters) или автоматически из Jenkinsfile после Build & push
|
// Триггер: вручную (Build with Parameters) или автоматически из Jenkinsfile после Build & push
|
||||||
// Credentials:
|
// Credentials (Global):
|
||||||
// k3s-kubeconfig — Secret file (kubeconfig), как у devops.org.ru
|
// k3s-kubeconfig — Secret file (kubeconfig к K3S)
|
||||||
//
|
//
|
||||||
// Agent: cloud kubernetes, label docker (pod template с container docker)
|
// Agent: cloud kubernetes, label docker (pod template с container docker)
|
||||||
//
|
//
|
||||||
@@ -34,8 +34,8 @@ pipeline {
|
|||||||
)
|
)
|
||||||
string(
|
string(
|
||||||
name: 'IMAGE_TAG',
|
name: 'IMAGE_TAG',
|
||||||
defaultValue: '0.1.0',
|
defaultValue: '',
|
||||||
description: 'Тег образа (0.1.0 / latest / short-sha после CI build)'
|
description: 'Тег образа (SemVer из CI). Пусто = cat VERSION из репозитория'
|
||||||
)
|
)
|
||||||
choice(
|
choice(
|
||||||
name: 'IMAGE_PULL_POLICY',
|
name: 'IMAGE_PULL_POLICY',
|
||||||
@@ -112,9 +112,15 @@ pipeline {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# IMAGE_TAG пустой → из VERSION (автоверсионирование)
|
||||||
|
if [ -z "${IMAGE_TAG}" ]; then
|
||||||
|
IMAGE_TAG="$(tr -d '[:space:]' < VERSION)"
|
||||||
|
export IMAGE_TAG
|
||||||
|
fi
|
||||||
|
test -n "${IMAGE_TAG}"
|
||||||
echo "Upgrading ${HELM_RELEASE}: ${IMAGE_REPOSITORY}:${IMAGE_TAG} (${IMAGE_PULL_POLICY})"
|
echo "Upgrading ${HELM_RELEASE}: ${IMAGE_REPOSITORY}:${IMAGE_TAG} (${IMAGE_PULL_POLICY})"
|
||||||
|
|
||||||
# Как addon: helm upgrade --install … --atomic --wait
|
# Как addon: helm upgrade … --atomic --wait
|
||||||
# --reuse-values сохраняет Ingress/секреты/DB/S3 с кластера
|
# --reuse-values сохраняет Ingress/секреты/DB/S3 с кластера
|
||||||
helm upgrade "$HELM_RELEASE" "$HELM_CHART" \
|
helm upgrade "$HELM_RELEASE" "$HELM_CHART" \
|
||||||
--namespace "$HELM_NAMESPACE" \
|
--namespace "$HELM_NAMESPACE" \
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
.PHONY: help env build up down restart logs shell migrate revision release buildx-setup helm-package helm-lint clean ps push
|
.PHONY: help env build up down restart logs shell migrate revision release buildx-setup helm-package helm-lint clean ps push bump-patch bump-minor version-commit
|
||||||
|
|
||||||
COMPOSE ?= docker compose
|
COMPOSE ?= docker compose
|
||||||
IMAGE_NAME ?= wrapped
|
IMAGE_NAME ?= wrapped
|
||||||
IMAGE_TAG ?= 0.1.0
|
# SemVer from VERSION (source of truth); override: make release IMAGE_TAG=1.2.3
|
||||||
|
IMAGE_TAG ?= $(shell cat VERSION 2>/dev/null || echo 0.1.0)
|
||||||
# Docker Hub namespace → image: $(RELEASE_REGISTRY)/$(IMAGE_NAME):$(IMAGE_TAG)
|
# Docker Hub namespace → image: $(RELEASE_REGISTRY)/$(IMAGE_NAME):$(IMAGE_TAG)
|
||||||
# Example: inecs/wrapped:0.1.0
|
# Example: inecs/wrapped:0.1.0
|
||||||
RELEASE_REGISTRY ?= inecs
|
RELEASE_REGISTRY ?= inecs
|
||||||
@@ -27,17 +28,21 @@ help:
|
|||||||
@echo " make shell Shell into app container"
|
@echo " make shell Shell into app container"
|
||||||
@echo " make migrate Run alembic upgrade head"
|
@echo " make migrate Run alembic upgrade head"
|
||||||
@echo " make revision m=\"msg\" Create alembic revision"
|
@echo " make revision m=\"msg\" Create alembic revision"
|
||||||
|
@echo " make bump-patch VERSION +0.0.1 (pyproject + Helm)"
|
||||||
|
@echo " make bump-minor VERSION +0.1.0"
|
||||||
@echo " make release Multi-arch build & push $(FULL_IMAGE)"
|
@echo " make release Multi-arch build & push $(FULL_IMAGE)"
|
||||||
@echo " platforms: $(RELEASE_PLATFORMS)"
|
@echo " platforms: $(RELEASE_PLATFORMS)"
|
||||||
@echo " make push git add ., commit (prompt), push origin"
|
@echo " make push bump-patch + git add/commit (prompt) + push"
|
||||||
@echo " make helm-lint Lint Helm chart"
|
@echo " make helm-lint Lint Helm chart"
|
||||||
@echo " make helm-package Package Helm chart"
|
@echo " make helm-package Package Helm chart"
|
||||||
@echo " make clean Remove containers, volumes, local image"
|
@echo " make clean Remove containers, volumes, local image"
|
||||||
@echo ""
|
@echo ""
|
||||||
|
@echo "Version: $$(cat VERSION 2>/dev/null || echo 0.1.0)"
|
||||||
@echo "Release examples:"
|
@echo "Release examples:"
|
||||||
@echo " make release IMAGE_TAG=0.1.0"
|
@echo " make release # tag from VERSION"
|
||||||
@echo " make release IMAGE_TAG=0.1.0 PUSH=0"
|
@echo " make release IMAGE_TAG=1.2.3"
|
||||||
@echo " make release IMAGE_TAG=0.1.0 RELEASE_PLATFORMS=linux/arm64"
|
@echo " make release PUSH=0"
|
||||||
|
@echo " make release RELEASE_PLATFORMS=linux/arm64"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@test -f .env || cp .env.example .env
|
@test -f .env || cp .env.example .env
|
||||||
@@ -131,8 +136,24 @@ clean:
|
|||||||
-docker buildx rm $(BUILDX_BUILDER) 2>/dev/null || true
|
-docker buildx rm $(BUILDX_BUILDER) 2>/dev/null || true
|
||||||
rm -rf dist
|
rm -rf dist
|
||||||
|
|
||||||
# Same flow as proxmox_api_simulator: stage all, multiline commit via Ctrl-D, push origin.
|
# Same flow as proxmox_api_simulator / infra: bump SemVer, stage, multiline commit, push.
|
||||||
push:
|
bump-patch:
|
||||||
|
@PYTHONPATH=. python3 scripts/bump_version.py patch
|
||||||
|
@echo "VERSION → $$(cat VERSION)"
|
||||||
|
|
||||||
|
bump-minor:
|
||||||
|
@PYTHONPATH=. python3 scripts/bump_version.py minor
|
||||||
|
@echo "VERSION → $$(cat VERSION)"
|
||||||
|
|
||||||
|
version-commit:
|
||||||
|
@git add VERSION pyproject.toml helm/wrapped/Chart.yaml helm/wrapped/values.yaml
|
||||||
|
@if git diff --cached --quiet; then \
|
||||||
|
echo "Version unchanged — no version commit"; \
|
||||||
|
else \
|
||||||
|
git commit -m "Bump version to $$(cat VERSION)."; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
push: bump-patch
|
||||||
@set -e; \
|
@set -e; \
|
||||||
git add .; \
|
git add .; \
|
||||||
echo "=== staged ==="; \
|
echo "=== staged ==="; \
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
| **Расшифровка** | Прокрутка превью текста по вертикали и горизонтали; кнопка **Копировать** рядом со скачиванием |
|
| **Расшифровка** | Прокрутка превью текста по вертикали и горизонтали; кнопка **Копировать** рядом со скачиванием |
|
||||||
| **Пароль** | Argon2-проверка *до* выдачи ciphertext; лимит попыток; пустой пароль попытку не тратит; структурированные ошибки в UI |
|
| **Пароль** | Argon2-проверка *до* выдачи ciphertext; лимит попыток; пустой пароль попытку не тратит; структурированные ошибки в UI |
|
||||||
| **Админка** | Стартовая **Статистика** (MinIO, wraps, audit); очистка audit с подтверждением; пагинация журнала |
|
| **Админка** | Стартовая **Статистика** (MinIO, wraps, audit); очистка audit с подтверждением; пагинация журнала |
|
||||||
| **Релиз** | `make release` / Jenkins CI+CD: multi-arch → Harbor и Docker Hub; Helm upgrade в k3s (`Jenkinsfile.deploy`, как addon-wrapped) |
|
| **Релиз** | Авто-SemVer (`VERSION`); Jenkins CI+CD: multi-arch → Harbor/Hub; Helm upgrade в k3s |
|
||||||
|
|
||||||
Подробности — в разделах ниже (безопасность, админка, сборка образа).
|
Подробности — в разделах ниже (безопасность, админка, сборка образа).
|
||||||
|
|
||||||
@@ -432,14 +432,30 @@ docker buildx build \
|
|||||||
| Harbor | `hub.antropoff.ru/devops-tools/wrapped` |
|
| Harbor | `hub.antropoff.ru/devops-tools/wrapped` |
|
||||||
| Docker Hub | `inecs/wrapped` |
|
| Docker Hub | `inecs/wrapped` |
|
||||||
|
|
||||||
Теги на каждый реестр: `:<short-sha>`, `:0.1.0`, `:latest` (плюс служебные `:<sha>-amd64` / `:<sha>-arm64`).
|
Теги на каждый реестр: `:<semver>` (ровно из `VERSION` в коммите), `:<short-sha>`, `:latest`.
|
||||||
|
|
||||||
Credentials (как в актуальном `Jenkinsfile`):
|
### Версионирование
|
||||||
|
|
||||||
|
Источник правды — файл [`VERSION`](VERSION) (SemVer). Синхронизируется в `pyproject.toml` и Helm (`Chart.yaml`, `values.yaml` image.tag).
|
||||||
|
|
||||||
|
| Где | Поведение |
|
||||||
|
|-----|-----------|
|
||||||
|
| UI (модалка «?») | бейдж `vX.Y.Z` |
|
||||||
|
| `make bump-patch` / `bump-minor` | ручной bump |
|
||||||
|
| `make push` | автоматически `bump-patch`, затем commit/push |
|
||||||
|
| Jenkins (`Jenkinsfile`) | читает `VERSION` из коммита **без** доп. bump → те же теги в образе и на кластере |
|
||||||
|
|
||||||
|
Что в `VERSION` запушили — то и уйдёт в Harbor/Hub/деплой (и в футер модалки внутри образа).
|
||||||
|
|
||||||
|
Credentials (Global, как в job’ах):
|
||||||
|
|
||||||
| ID | Тип | Назначение |
|
| ID | Тип | Назначение |
|
||||||
|----|-----|------------|
|
|----|-----|------------|
|
||||||
| `harbor-devops-tools-push-pull-access` | Username/password | Harbor `devops-tools` |
|
| `ssh-gitea-key` | SSH Username with private key | SCM checkout |
|
||||||
| `docker-hub` | Username/password | Docker Hub |
|
| `gitea-jenkins-token` | Secret text / token | API Gitea (webhook/API при необходимости) |
|
||||||
|
| `harbor-devops-tools-push-pull-access` | Username/password | Harbor `devops-tools` (robot) |
|
||||||
|
| `docker-hub` | Username/password | Docker Hub `inecs` |
|
||||||
|
| `k3s-kubeconfig` | Secret file | Helm/kubectl deploy в K3S |
|
||||||
|
|
||||||
Agent: label `docker` (pod template с container `docker` / DinD).
|
Agent: label `docker` (pod template с container `docker` / DinD).
|
||||||
|
|
||||||
@@ -471,14 +487,14 @@ kubectl -n wrapped rollout status deployment/wrapped
|
|||||||
| Параметр job | Умолч. | Смысл |
|
| Параметр job | Умолч. | Смысл |
|
||||||
|--------------|--------|--------|
|
|--------------|--------|--------|
|
||||||
| `IMAGE_REPOSITORY` | `inecs/wrapped` | как `wrapped_image_repository` |
|
| `IMAGE_REPOSITORY` | `inecs/wrapped` | как `wrapped_image_repository` |
|
||||||
| `IMAGE_TAG` | `0.1.0` | как `wrapped_image_tag` (`latest` / short-sha после build) |
|
| `IMAGE_TAG` | из `VERSION` / CI | SemVer образа (`Always` подтянет новый) |
|
||||||
| `IMAGE_PULL_POLICY` | `Always` | как в addon |
|
| `IMAGE_PULL_POLICY` | `Always` | как в addon |
|
||||||
|
|
||||||
Credential: `k3s-kubeconfig` (Secret file) — тот же, что у devops.org.ru.
|
Credential: `k3s-kubeconfig` (Secret file) — kubeconfig к K3S.
|
||||||
|
|
||||||
Рекомендуемая связка job’ов уже в [`Jenkinsfile`](Jenkinsfile): после успешного **Build & push** стадия **Trigger deploy** вызывает Multibranch job `Devops Tools/Wrapped Deploy/main` с параметрами `inecs/wrapped` / `0.1.0` / `Always` (`wait: true`). Если имя folder/job другое — поменяй `DEPLOY_JOB` в `Jenkinsfile`.
|
Рекомендуемая связка job’ов уже в [`Jenkinsfile`](Jenkinsfile): после успешного **Build image** (DinD) стадия **Trigger deploy** с `wait: false` и `agent none` — под Builder освобождается, затем отдельно стартует Deploy. Так на кластере не нужны два DinD одновременно.
|
||||||
|
|
||||||
Ручной запуск Deploy по-прежнему: **Build with Parameters**.
|
Ручной запуск Deploy: **Build with Parameters** (пустой `IMAGE_TAG` → `cat VERSION`).
|
||||||
|
|
||||||
### Git (`make push`)
|
### Git (`make push`)
|
||||||
|
|
||||||
|
|||||||
+13
-2
@@ -14,6 +14,7 @@ from app.config import get_settings
|
|||||||
from app.db import SessionLocal
|
from app.db import SessionLocal
|
||||||
from app.services.settings_service import get_or_create_settings
|
from app.services.settings_service import get_or_create_settings
|
||||||
from app.services.storage import storage
|
from app.services.storage import storage
|
||||||
|
from app.version import get_app_version, get_app_version_label
|
||||||
|
|
||||||
OPENAPI_TAGS = [
|
OPENAPI_TAGS = [
|
||||||
{
|
{
|
||||||
@@ -73,9 +74,10 @@ def custom_openapi(app: FastAPI):
|
|||||||
def create_app() -> FastAPI:
|
def create_app() -> FastAPI:
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
docs_on = settings.is_docs_enabled
|
docs_on = settings.is_docs_enabled
|
||||||
|
app_version = get_app_version()
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title=settings.app_name,
|
title=settings.app_name,
|
||||||
version="0.1.0",
|
version=app_version,
|
||||||
description=(
|
description=(
|
||||||
"Wrapped — zero-knowledge one-time encrypted drop.\n\n"
|
"Wrapped — zero-knowledge one-time encrypted drop.\n\n"
|
||||||
"Public wrap APIs are anonymous. Admin APIs require HTTP Basic."
|
"Public wrap APIs are anonymous. Admin APIs require HTTP Basic."
|
||||||
@@ -97,10 +99,19 @@ def create_app() -> FastAPI:
|
|||||||
app.include_router(admin.api_router)
|
app.include_router(admin.api_router)
|
||||||
|
|
||||||
templates = Jinja2Templates(directory="app/templates")
|
templates = Jinja2Templates(directory="app/templates")
|
||||||
|
# Callables so Jinja re-reads VERSION (dev mount / after bump) on each render
|
||||||
|
templates.env.globals["app_version"] = get_app_version
|
||||||
|
templates.env.globals["app_version_label"] = get_app_version_label
|
||||||
|
admin.templates.env.globals["app_version"] = get_app_version
|
||||||
|
admin.templates.env.globals["app_version_label"] = get_app_version_label
|
||||||
|
|
||||||
@app.get("/health", tags=["System"], summary="Health check", include_in_schema=docs_on)
|
@app.get("/health", tags=["System"], summary="Health check", include_in_schema=docs_on)
|
||||||
async def health():
|
async def health():
|
||||||
return {"status": "ok", "service": settings.app_name}
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"service": settings.app_name,
|
||||||
|
"version": get_app_version(),
|
||||||
|
}
|
||||||
|
|
||||||
@app.get("/", include_in_schema=False)
|
@app.get("/", include_in_schema=False)
|
||||||
async def index(request: Request):
|
async def index(request: Request):
|
||||||
|
|||||||
+58
-88
@@ -201,6 +201,10 @@ html[data-theme="dark"] .icon-btn:hover {
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
.icon { width: 18px; height: 18px; }
|
.icon { width: 18px; height: 18px; }
|
||||||
|
.icon-btn i {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
.hidden { display: none !important; }
|
.hidden { display: none !important; }
|
||||||
|
|
||||||
.shell {
|
.shell {
|
||||||
@@ -784,108 +788,24 @@ html[data-theme="light"] .form-alert-attempts {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
padding: 0.5rem 1.25rem 2.25rem;
|
padding: 0.75rem 1.25rem 2rem;
|
||||||
max-width: 760px;
|
max-width: 760px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
.footer-pillars {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.55rem 0.7rem;
|
|
||||||
}
|
|
||||||
.footer-pillars li {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.55rem;
|
|
||||||
padding: 0.55rem 0.85rem;
|
|
||||||
border: 1px solid rgba(110, 168, 255, 0.22);
|
|
||||||
border-radius: 12px;
|
|
||||||
background: linear-gradient(160deg, rgba(110, 168, 255, 0.1), rgba(13, 20, 32, 0.35));
|
|
||||||
color: var(--text);
|
|
||||||
text-align: left;
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
max-width: 240px;
|
|
||||||
}
|
|
||||||
html[data-theme="light"] .footer-pillars li {
|
|
||||||
background: linear-gradient(160deg, rgba(47, 111, 237, 0.1), rgba(255, 255, 255, 0.85));
|
|
||||||
border-color: rgba(47, 111, 237, 0.2);
|
|
||||||
}
|
|
||||||
.footer-pillars i {
|
|
||||||
color: var(--accent-2);
|
|
||||||
font-size: 0.95rem;
|
|
||||||
width: 1.1rem;
|
|
||||||
text-align: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.footer-pillars span {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.1rem;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
.footer-pillars strong {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 650;
|
|
||||||
letter-spacing: 0.01em;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
.footer-pillars small {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
color: var(--muted);
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1.25;
|
|
||||||
}
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
.site-footer {
|
.site-footer {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
padding: 0.35rem 0.6rem 1.5rem;
|
padding: 0.5rem 0.85rem 1.5rem;
|
||||||
}
|
|
||||||
.footer-pillars {
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
gap: 0.3rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.footer-pillars li {
|
|
||||||
flex: 1 1 0;
|
|
||||||
max-width: none;
|
|
||||||
min-width: 0;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
gap: 0.2rem;
|
|
||||||
padding: 0.4rem 0.3rem;
|
|
||||||
border-radius: 9px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.footer-pillars i {
|
|
||||||
font-size: 0.72rem;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
.footer-pillars span {
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.05rem;
|
|
||||||
}
|
|
||||||
.footer-pillars strong {
|
|
||||||
font-size: 0.62rem;
|
|
||||||
line-height: 1.15;
|
|
||||||
}
|
|
||||||
.footer-pillars small {
|
|
||||||
font-size: 0.55rem;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
}
|
||||||
.footer-copy {
|
.footer-copy {
|
||||||
margin-top: 0.65rem;
|
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.footer-copy {
|
.footer-copy {
|
||||||
margin: 0.85rem 0 0;
|
margin: 0;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
.footer-copy a {
|
.footer-copy a {
|
||||||
color: var(--accent-2);
|
color: var(--accent-2);
|
||||||
@@ -896,6 +816,56 @@ html[data-theme="light"] .footer-pillars li {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.about-modal-panel {
|
||||||
|
width: min(624px, 100%);
|
||||||
|
}
|
||||||
|
.about-modal-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.85rem;
|
||||||
|
margin-bottom: 0.15rem;
|
||||||
|
}
|
||||||
|
.about-modal-head .modal-title {
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.about-version {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 0.15rem;
|
||||||
|
padding: 0.28rem 0.65rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(110, 168, 255, 0.28);
|
||||||
|
background: linear-gradient(160deg, rgba(110, 168, 255, 0.16), rgba(61, 214, 198, 0.08));
|
||||||
|
color: var(--accent-2);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .about-version {
|
||||||
|
border-color: rgba(47, 111, 237, 0.25);
|
||||||
|
background: linear-gradient(160deg, rgba(47, 111, 237, 0.12), rgba(14, 160, 140, 0.08));
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.about-modal-body {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin: 0.85rem 0 1.1rem;
|
||||||
|
}
|
||||||
|
.about-modal-body p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.about-modal-panel .modal-actions {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.sr-only {
|
.sr-only {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 1px; height: 1px;
|
width: 1px; height: 1px;
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
(() => {
|
||||||
|
const modal = document.getElementById("about-modal");
|
||||||
|
const toggle = document.getElementById("about-toggle");
|
||||||
|
if (!modal || !toggle) return;
|
||||||
|
|
||||||
|
function syncToggleLabels() {
|
||||||
|
const label = window.WrappedI18n?.t("about.open") || "About Wrapped";
|
||||||
|
toggle.setAttribute("title", label);
|
||||||
|
toggle.setAttribute("aria-label", label);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openModal() {
|
||||||
|
modal.classList.remove("hidden");
|
||||||
|
document.body.classList.add("modal-open");
|
||||||
|
modal.querySelector("[data-about-close].btn")?.focus?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
modal.classList.add("hidden");
|
||||||
|
document.body.classList.remove("modal-open");
|
||||||
|
toggle.focus?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle.addEventListener("click", openModal);
|
||||||
|
modal.addEventListener("click", (e) => {
|
||||||
|
if (e.target.closest("[data-about-close]")) closeModal();
|
||||||
|
});
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Escape" && !modal.classList.contains("hidden")) closeModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (window.WrappedI18n?.onChange) {
|
||||||
|
window.WrappedI18n.onChange(syncToggleLabels);
|
||||||
|
}
|
||||||
|
syncToggleLabels();
|
||||||
|
})();
|
||||||
+14
-12
@@ -10,13 +10,14 @@
|
|||||||
"footer.cap.media.hint": "Screenshots, archives, docs",
|
"footer.cap.media.hint": "Screenshots, archives, docs",
|
||||||
"footer.cap.browser": "Encrypted in-browser",
|
"footer.cap.browser": "Encrypted in-browser",
|
||||||
"footer.cap.browser.hint": "Only ciphertext reaches the server",
|
"footer.cap.browser.hint": "Only ciphertext reaches the server",
|
||||||
"footer.pillar.zk": "Zero-knowledge",
|
|
||||||
"footer.pillar.once": "One-time",
|
|
||||||
"footer.pillar.encrypted": "Encrypted",
|
|
||||||
"footer.pillar.zk.hint": "Server never sees content",
|
|
||||||
"footer.pillar.once.hint": "Gone after unwrap",
|
|
||||||
"footer.pillar.encrypted.hint": "Key lives only in your link",
|
|
||||||
"footer.copy.author": "Sergey Antropov",
|
"footer.copy.author": "Sergey Antropov",
|
||||||
|
"about.open": "About Wrapped",
|
||||||
|
"about.title": "About Wrapped",
|
||||||
|
"about.p1": "Wrapped is a one-time secure drop for text, images and files.",
|
||||||
|
"about.p2": "Send a note or code snippet, and attachments too: documents, archives, screenshots (drag-and-drop, file picker, or paste from the clipboard). Both text and files are encrypted in the browser before upload — only ciphertext reaches the server.",
|
||||||
|
"about.p3": "The server never sees plaintext: encrypted data stays on the server until the recipient opens the link with the key and decrypts the package.",
|
||||||
|
"about.p4": "After a successful unwrap the server copy is destroyed. The encryption key lives in the URL fragment (#…) and is not sent to the server with the page request. Optionally, a wrap can also be protected with a password.",
|
||||||
|
"about.close": "Got it",
|
||||||
"brand.tagline": "Wrap. Send. Vanish.",
|
"brand.tagline": "Wrap. Send. Vanish.",
|
||||||
"create.eyebrow": "Secure drop",
|
"create.eyebrow": "Secure drop",
|
||||||
"create.title": "Wrap. Send. Vanish.",
|
"create.title": "Wrap. Send. Vanish.",
|
||||||
@@ -227,13 +228,14 @@
|
|||||||
"footer.cap.media.hint": "Скриншоты, архивы, документы",
|
"footer.cap.media.hint": "Скриншоты, архивы, документы",
|
||||||
"footer.cap.browser": "Шифрование в браузере",
|
"footer.cap.browser": "Шифрование в браузере",
|
||||||
"footer.cap.browser.hint": "На сервер уходит только ciphertext",
|
"footer.cap.browser.hint": "На сервер уходит только ciphertext",
|
||||||
"footer.pillar.zk": "Zero-knowledge",
|
|
||||||
"footer.pillar.once": "Одноразово",
|
|
||||||
"footer.pillar.encrypted": "Зашифровано",
|
|
||||||
"footer.pillar.zk.hint": "Сервер не видит содержимое",
|
|
||||||
"footer.pillar.once.hint": "После открытия — удаление",
|
|
||||||
"footer.pillar.encrypted.hint": "Ключ только в вашей ссылке",
|
|
||||||
"footer.copy.author": "Сергей Антропов",
|
"footer.copy.author": "Сергей Антропов",
|
||||||
|
"about.open": "О проекте Wrapped",
|
||||||
|
"about.title": "О проекте Wrapped",
|
||||||
|
"about.p1": "Wrapped — сервис одноразовой безопасной передачи текста, изображений и файлов.",
|
||||||
|
"about.p2": "Можно отправить заметку или код, а также вложения: документы, архивы, скриншоты (drag-and-drop, выбор с диска или вставка из буфера). И текст, и файлы шифруются в браузере до загрузки — на сервер уходит только ciphertext.",
|
||||||
|
"about.p3": "Сервер никогда не видит plaintext: зашифрованные данные лежат на сервере до тех пор, пока получатель не откроет ссылку с ключом и не расшифрует пакет.",
|
||||||
|
"about.p4": "После успешной расшифровки копия на сервере уничтожается. Ключ шифрования живёт во фрагменте URL (#…) и не уходит на сервер вместе с запросом страницы. При желании wrap можно дополнительно защитить паролем.",
|
||||||
|
"about.close": "Понятно",
|
||||||
"brand.tagline": "Упакуй. Отправь. Исчезни.",
|
"brand.tagline": "Упакуй. Отправь. Исчезни.",
|
||||||
"create.eyebrow": "Безопасная передача",
|
"create.eyebrow": "Безопасная передача",
|
||||||
"create.title": "Упакуй. Отправь. Исчезни.",
|
"create.title": "Упакуй. Отправь. Исчезни.",
|
||||||
|
|||||||
+24
-23
@@ -36,44 +36,45 @@
|
|||||||
<svg id="theme-icon-sun" class="icon hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
|
<svg id="theme-icon-sun" class="icon hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
|
||||||
<svg id="theme-icon-moon" class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M21 14.5A8.5 8.5 0 1 1 9.5 3a7 7 0 0 0 11.5 11.5z"/></svg>
|
<svg id="theme-icon-moon" class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M21 14.5A8.5 8.5 0 1 1 9.5 3a7 7 0 0 0 11.5 11.5z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="icon-btn" id="about-toggle" title="О проекте Wrapped" aria-label="О проекте Wrapped">
|
||||||
|
<i class="fa-solid fa-circle-question" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main class="shell">
|
<main class="shell">
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
<footer class="site-footer">
|
<footer class="site-footer">
|
||||||
<ul class="footer-pillars">
|
|
||||||
<li>
|
|
||||||
<i class="fa-solid fa-user-secret" aria-hidden="true"></i>
|
|
||||||
<span>
|
|
||||||
<strong data-i18n="footer.pillar.zk">Zero-knowledge</strong>
|
|
||||||
<small data-i18n="footer.pillar.zk.hint">Сервер не видит содержимое</small>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<i class="fa-solid fa-fire" aria-hidden="true"></i>
|
|
||||||
<span>
|
|
||||||
<strong data-i18n="footer.pillar.once">Одноразово</strong>
|
|
||||||
<small data-i18n="footer.pillar.once.hint">После открытия — удаление</small>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<i class="fa-solid fa-lock" aria-hidden="true"></i>
|
|
||||||
<span>
|
|
||||||
<strong data-i18n="footer.pillar.encrypted">Зашифровано</strong>
|
|
||||||
<small data-i18n="footer.pillar.encrypted.hint">Ключ только в вашей ссылке</small>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p class="footer-copy">
|
<p class="footer-copy">
|
||||||
© <span data-i18n="footer.copy.author">Сергей Антропов</span>
|
© <span data-i18n="footer.copy.author">Сергей Антропов</span>
|
||||||
· <a href="https://devops.org.ru" target="_blank" rel="noopener noreferrer">devops.org.ru</a>
|
· <a href="https://devops.org.ru" target="_blank" rel="noopener noreferrer">devops.org.ru</a>
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<div id="about-modal" class="modal-root hidden" role="dialog" aria-modal="true" aria-labelledby="about-modal-title">
|
||||||
|
<div class="modal-backdrop" data-about-close></div>
|
||||||
|
<div class="modal-panel about-modal-panel" role="document">
|
||||||
|
<header class="about-modal-head">
|
||||||
|
<h2 class="modal-title" id="about-modal-title" data-i18n="about.title">О проекте Wrapped</h2>
|
||||||
|
<span class="about-version" title="Version">{{ app_version_label() }}</span>
|
||||||
|
</header>
|
||||||
|
<div class="about-modal-body">
|
||||||
|
<p data-i18n="about.p1">Wrapped — сервис одноразовой безопасной передачи текста, изображений и файлов.</p>
|
||||||
|
<p data-i18n="about.p2">Можно отправить заметку или код, а также вложения: документы, архивы, скриншоты (drag-and-drop, выбор с диска или вставка из буфера). И текст, и файлы шифруются в браузере до загрузки — на сервер уходит только ciphertext.</p>
|
||||||
|
<p data-i18n="about.p3">Сервер никогда не видит plaintext: зашифрованные данные лежат на сервере до тех пор, пока получатель не откроет ссылку с ключом и не расшифрует пакет.</p>
|
||||||
|
<p data-i18n="about.p4">После успешной расшифровки копия на сервере уничтожается. Ключ шифрования живёт во фрагменте URL (#…) и не уходит на сервер вместе с запросом страницы. При желании wrap можно дополнительно защитить паролем.</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button type="button" class="btn primary" data-about-close data-i18n="about.close">Понятно</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
<script src="/static/js/i18n.js"></script>
|
<script src="/static/js/i18n.js"></script>
|
||||||
<script src="/static/js/theme.js"></script>
|
<script src="/static/js/theme.js"></script>
|
||||||
<script src="/static/js/ui.js"></script>
|
<script src="/static/js/ui.js"></script>
|
||||||
|
<script src="/static/js/about.js"></script>
|
||||||
<script src="/static/js/highlight-ui.js"></script>
|
<script src="/static/js/highlight-ui.js"></script>
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from functools import lru_cache
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
_VERSION_FILE = Path(__file__).resolve().parents[1] / "VERSION"
|
||||||
|
_PROJECT_ROOT = _VERSION_FILE.parent
|
||||||
|
_PYPROJECT_FILE = _PROJECT_ROOT / "pyproject.toml"
|
||||||
|
_CHART_FILE = _PROJECT_ROOT / "helm" / "wrapped" / "Chart.yaml"
|
||||||
|
_VALUES_FILE = _PROJECT_ROOT / "helm" / "wrapped" / "values.yaml"
|
||||||
|
_DEFAULT_VERSION = "0.1.0"
|
||||||
|
_VERSION_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+)$")
|
||||||
|
|
||||||
|
|
||||||
|
def _read_version_file(path: Path) -> str | None:
|
||||||
|
try:
|
||||||
|
text = path.read_text(encoding="utf-8").strip()
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
if _VERSION_RE.match(text):
|
||||||
|
return text
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache
|
||||||
|
def get_app_version() -> str:
|
||||||
|
for path in (
|
||||||
|
Path("/app/VERSION"),
|
||||||
|
_VERSION_FILE,
|
||||||
|
Path.cwd() / "VERSION",
|
||||||
|
):
|
||||||
|
found = _read_version_file(path)
|
||||||
|
if found:
|
||||||
|
return found
|
||||||
|
try:
|
||||||
|
from importlib.metadata import version
|
||||||
|
|
||||||
|
meta = version("wrapped")
|
||||||
|
if _VERSION_RE.match(meta):
|
||||||
|
return meta
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return _DEFAULT_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
def get_app_version_label() -> str:
|
||||||
|
clear_version_cache()
|
||||||
|
return f"v{get_app_version()}"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_version(value: str) -> tuple[int, int, int]:
|
||||||
|
match = _VERSION_RE.match(value.strip())
|
||||||
|
if not match:
|
||||||
|
raise ValueError(f"Invalid semantic version: {value!r}")
|
||||||
|
return int(match.group(1)), int(match.group(2)), int(match.group(3))
|
||||||
|
|
||||||
|
|
||||||
|
def format_version(major: int, minor: int, patch: int) -> str:
|
||||||
|
return f"{major}.{minor}.{patch}"
|
||||||
|
|
||||||
|
|
||||||
|
def clear_version_cache() -> None:
|
||||||
|
get_app_version.cache_clear()
|
||||||
|
|
||||||
|
|
||||||
|
def write_project_version(version: str) -> None:
|
||||||
|
parse_version(version)
|
||||||
|
_VERSION_FILE.write_text(f"{version}\n", encoding="utf-8")
|
||||||
|
|
||||||
|
pyproject = _PYPROJECT_FILE.read_text(encoding="utf-8")
|
||||||
|
pyproject, count = re.subn(
|
||||||
|
r'^version = ".*"$',
|
||||||
|
f'version = "{version}"',
|
||||||
|
pyproject,
|
||||||
|
count=1,
|
||||||
|
flags=re.MULTILINE,
|
||||||
|
)
|
||||||
|
if count != 1:
|
||||||
|
raise RuntimeError("Failed to update pyproject.toml version")
|
||||||
|
_PYPROJECT_FILE.write_text(pyproject, encoding="utf-8")
|
||||||
|
|
||||||
|
chart = _CHART_FILE.read_text(encoding="utf-8")
|
||||||
|
chart, chart_count = re.subn(
|
||||||
|
r"^version: .*$",
|
||||||
|
f"version: {version}",
|
||||||
|
chart,
|
||||||
|
count=1,
|
||||||
|
flags=re.MULTILINE,
|
||||||
|
)
|
||||||
|
chart, app_count = re.subn(
|
||||||
|
r'^appVersion: ".*"$',
|
||||||
|
f'appVersion: "{version}"',
|
||||||
|
chart,
|
||||||
|
count=1,
|
||||||
|
flags=re.MULTILINE,
|
||||||
|
)
|
||||||
|
if chart_count != 1 or app_count != 1:
|
||||||
|
raise RuntimeError("Failed to update Chart.yaml version")
|
||||||
|
_CHART_FILE.write_text(chart, encoding="utf-8")
|
||||||
|
|
||||||
|
values = _VALUES_FILE.read_text(encoding="utf-8")
|
||||||
|
values, values_count = re.subn(
|
||||||
|
r'^(\s*tag:\s*)".*"$',
|
||||||
|
rf'\1"{version}"',
|
||||||
|
values,
|
||||||
|
count=1,
|
||||||
|
flags=re.MULTILINE,
|
||||||
|
)
|
||||||
|
if values_count != 1:
|
||||||
|
raise RuntimeError("Failed to update helm values.yaml image.tag")
|
||||||
|
_VALUES_FILE.write_text(values, encoding="utf-8")
|
||||||
|
|
||||||
|
clear_version_cache()
|
||||||
|
|
||||||
|
|
||||||
|
def bump_patch_version() -> str:
|
||||||
|
major, minor, patch = parse_version(get_app_version())
|
||||||
|
version = format_version(major, minor, patch + 1)
|
||||||
|
write_project_version(version)
|
||||||
|
return version
|
||||||
|
|
||||||
|
|
||||||
|
def bump_minor_version() -> str:
|
||||||
|
major, minor, patch = parse_version(get_app_version())
|
||||||
|
version = format_version(major, minor + 1, 0)
|
||||||
|
write_project_version(version)
|
||||||
|
return version
|
||||||
@@ -40,6 +40,7 @@ services:
|
|||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
volumes:
|
volumes:
|
||||||
- ./app:/app/app:ro
|
- ./app:/app/app:ro
|
||||||
|
- ./VERSION:/app/VERSION:ro
|
||||||
command: >
|
command: >
|
||||||
sh -c "alembic upgrade head &&
|
sh -c "alembic upgrade head &&
|
||||||
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ apiVersion: v2
|
|||||||
name: wrapped
|
name: wrapped
|
||||||
description: Zero-knowledge one-time encrypted drop (Wrapped)
|
description: Zero-knowledge one-time encrypted drop (Wrapped)
|
||||||
type: application
|
type: application
|
||||||
version: 0.1.0
|
version: 0.1.2
|
||||||
appVersion: "0.1.0"
|
appVersion: "0.1.2"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ replicaCount: 1
|
|||||||
|
|
||||||
image:
|
image:
|
||||||
repository: inecs/wrapped
|
repository: inecs/wrapped
|
||||||
tag: "0.1.0"
|
tag: "0.1.2"
|
||||||
pullPolicy: IfNotPresent
|
pullPolicy: IfNotPresent
|
||||||
|
|
||||||
service:
|
service:
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "wrapped"
|
name = "wrapped"
|
||||||
version = "0.1.0"
|
version = "0.1.2"
|
||||||
description = "Zero-knowledge one-time encrypted drop service"
|
description = "Zero-knowledge one-time encrypted drop service"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""CLI: bump project SemVer (VERSION + pyproject + Helm chart/values)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
if str(ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
from app.version import bump_minor_version, bump_patch_version # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
args = argv if argv is not None else sys.argv[1:]
|
||||||
|
if len(args) != 1 or args[0] not in {"patch", "minor"}:
|
||||||
|
print("Usage: bump_version.py patch|minor", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
version = bump_patch_version() if args[0] == "patch" else bump_minor_version()
|
||||||
|
print(version)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user