Added soft page

This commit is contained in:
Magnus Root 2026-07-27 16:45:21 +03:00
parent 111af09ec7
commit 2c702f1f92
11 changed files with 412 additions and 4 deletions

View file

@ -15,6 +15,8 @@
1. go Справочник по языку Go
Установка, синтаксис, типы, функции, горутины...
5. soft Справочник по софту
Обзоры используемого софта: Zola, Forgejo...
```
---

View file

@ -23,6 +23,7 @@ var All = []Book{
LinuxBook,
DevOpsBook,
InfoSecBook,
SoftBook,
}
// Find ищет книгу по ключу или алиасу

View file

@ -6,6 +6,7 @@ import (
"rubric/modules/gomodules"
"rubric/modules/infosecmodules"
"rubric/modules/linuxmodules"
"rubric/modules/softmodules"
"rubric/utils"
"sort"
"strings"
@ -79,6 +80,16 @@ func buildSearchIndex() []searchEntry {
})
}
// Soft
for _, m := range softmodules.All {
entries = append(entries, searchEntry{
bookKey: "soft", bookTitle: "Soft",
modKey: m.Key, modTitle: m.Title, modDesc: m.Description,
keywords: softmodules.Keywords(m.Key),
run: m.Run,
})
}
return entries
}
@ -196,6 +207,8 @@ func bookColorFor(key string) string {
return utils.ColorMagenta
case "infosec":
return utils.ColorRed
case "soft":
return utils.ColorBlue
}
return utils.ColorWhite
}

99
catalog/softbook.go Normal file
View file

@ -0,0 +1,99 @@
package catalog
import (
"bufio"
"fmt"
"rubric/modules/softmodules"
"rubric/utils"
"strconv"
"strings"
)
var SoftBook = Book{
Key: "soft",
Aliases: []string{"sw", "software"},
Title: "Справочник по софту",
Description: "Обзоры используемого софта: Zola, Forgejo и другое",
Run: runSoftBook,
}
func runSoftBook(scanner *bufio.Scanner) {
for {
showSoftMenu()
input, ok := ReadLine(scanner)
if !ok {
return
}
if input == "" {
continue
}
if IsQuit(input) {
fmt.Printf("\n %s%sДо свидания!%s\n\n",
utils.ColorBold, utils.ColorCyan, utils.ColorReset)
return
}
if IsBack(input) {
return
}
if n, err := strconv.Atoi(input); err == nil {
if n >= 1 && n <= len(softmodules.All) {
softmodules.All[n-1].Run()
if softWaitBack(scanner) {
return
}
continue
}
fmt.Printf("\n %s%sНомер %d вне диапазона (1%d)%s\n",
utils.ColorBold, utils.ColorRed, n, len(softmodules.All), utils.ColorReset)
continue
}
if m, ok := softmodules.Find(input); ok {
m.Run()
if softWaitBack(scanner) {
return
}
continue
}
fmt.Printf("\n %s%sНе понял '%s'. Номер, ключ раздела, b — назад.%s\n",
utils.ColorBold, utils.ColorRed, input, utils.ColorReset)
}
}
// softWaitBack возвращает true если пользователь нажал b (выйти в главное меню)
func softWaitBack(scanner *bufio.Scanner) bool {
fmt.Printf("\n %s%s[Enter — назад в меню Soft, b — главное меню]%s ",
utils.ColorDim, utils.ColorCyan, utils.ColorReset)
if scanner.Scan() {
return IsBack(strings.TrimSpace(scanner.Text()))
}
return false
}
func showSoftMenu() {
utils.Header("Справочник Soft")
fmt.Printf(" %s%sРазделы:%s\n\n", utils.ColorBold, utils.ColorWhite, utils.ColorReset)
for i, m := range softmodules.All {
aliases := ""
if len(m.Aliases) > 0 {
aliases = fmt.Sprintf("%s [%s]%s",
utils.ColorDim, strings.Join(m.Aliases, ", "), utils.ColorReset)
}
fmt.Printf(" %s%s %2d.%s %s%-10s%s %s%-32s%s%s\n",
utils.ColorBold, utils.ColorCyan, i+1, utils.ColorReset,
utils.ColorYellow+utils.ColorBold, m.Key, utils.ColorReset,
utils.ColorWhite, m.Title, utils.ColorReset,
aliases,
)
fmt.Printf(" %s%s%s\n\n",
utils.ColorDim, m.Description, utils.ColorReset)
}
fmt.Printf(" %s%sРаздел (номер / ключ / b — главное меню / q — выход):%s ",
utils.ColorBold, utils.ColorGreen, utils.ColorReset)
}

12
main.go
View file

@ -10,12 +10,13 @@ import (
"rubric/modules/gomodules"
"rubric/modules/infosecmodules"
"rubric/modules/linuxmodules"
"rubric/modules/softmodules"
"rubric/utils"
"strings"
)
func main() {
book := flag.String("book", "", "открыть справочник (go, linux, devops, infosec)")
book := flag.String("book", "", "открыть справочник (go, linux, devops, infosec, soft)")
topic := flag.String("topic", "", "открыть раздел (используется вместе с -book)")
search := flag.String("search", "", "поиск по всем справочникам")
list := flag.Bool("list", false, "список всех справочников")
@ -102,6 +103,15 @@ func main() {
os.Exit(1)
}
m.Run()
case "soft", "sw", "software":
m, ok := softmodules.Find(*topic)
if !ok {
fmt.Printf("\n %s%sРаздел '%s' не найден в Soft.%s\n",
utils.ColorBold, utils.ColorRed, *topic, utils.ColorReset)
softmodules.ListModules()
os.Exit(1)
}
m.Run()
default:
fmt.Printf("\n %s%sСправочник '%s' не найден.%s\n",
utils.ColorBold, utils.ColorRed, *book, utils.ColorReset)

View file

@ -92,6 +92,8 @@ func Keywords(key string) []string {
"provider", "ресурс", "resource", "state", "план", "plan", "apply",
"destroy", "модуль", "module", "workspace", "backend", "S3",
"AWS", "GCP", "Azure", "переменные", "output",
"opentofu", "tofu", "форк", "fork", "MPL", "BSL", "лицензия",
"license", "Linux Foundation", "state encryption", "шифрование state",
},
"monitoring": {
"мониторинг", "Prometheus", "Grafana", "AlertManager", "Loki",

View file

@ -6,9 +6,9 @@ import (
var TerraformModule = Module{
Key: "terraform",
Aliases: []string{"tf", "iac", "infra"},
Title: "Terraform — инфраструктура как код",
Description: "HCL, провайдеры, ресурсы, state, модули, workspace",
Aliases: []string{"tf", "iac", "infra", "opentofu", "tofu"},
Title: "Terraform / OpenTofu — инфраструктура как код",
Description: "HCL, провайдеры, ресурсы, state, модули, workspace, форк OpenTofu",
Run: showTerraform,
}
@ -255,6 +255,61 @@ terraform workspace new staging
terraform workspace select production
# В коде: terraform.workspace "staging" / "production"`)
utils.Section("OpenTofu — форк Terraform")
utils.Code(`OpenTofu открытый форк Terraform, созданный в 2023 году после того,
как HashiCorp сменил лицензию Terraform с MPL 2.0 на BSL 1.1 (Business Source License).
Форк собран из последней MPL-версии Terraform (1.5.x) и живёт под управлением
Linux Foundation то есть под вендор-нейтральным, многосторонним управлением,
а не под контролем одной компании.
# Установка:
curl --proto '=https' --tlsv1.2 -fsSL \
https://get.opentofu.org/install-opentofu.sh -o install-opentofu.sh
chmod +x install-opentofu.sh
./install-opentofu.sh --install-method standalone
tofu version
# Совместимость: тот же HCL, тот же формат state, те же провайдеры
# миграция практически всегда сводится к замене бинаря:
alias terraform=tofu`)
utils.Section("Почему в 2026 лучше выбрать OpenTofu")
utils.Code(`1. Лицензия
OpenTofu MPL 2.0, одобрена OSI, без коммерческих ограничений.
Terraform (с версии 1.6) BSL 1.1, не одобрена OSI. BSL не мешает
обычному использованию "terraform apply" для своей инфраструктуры,
но запрещает предлагать Terraform как конкурирующий управляемый сервис
это касается вендоров SaaS/managed-платформ, не рядовых инженеров.
2. Управление (governance)
OpenTofu развивается комитетом под эгидой Linux Foundation
роадмап не завязан на коммерческие приоритеты одной компании.
Terraform принадлежит HashiCorp (с 2025 года в составе IBM).
3. Функции, которых нет в открытом Terraform CLI:
- нативное шифрование state и plan-файлов (KMS/PBKDF2)
важно, если бэкенду state нельзя полностью доверять
- provider for_each итерация по нескольким конфигурациям одного провайдера
- раннее вычисление переменных, в т.ч. в конфигурации backend
- флаг -exclude (точечное исключение ресурсов из plan/apply)
- собственный открытый реестр провайдеров/модулей registry.opentofu.org
4. Открытость без риска vendor lock-in
Если однажды понадобится вернуться на Terraform это тоже
практически замена бинаря, конфигурация и state совместимы.
Что осталось только в Terraform:
Terraform Stacks и часть функций тесно интегрированы с HCP Terraform
(управляемой платформой HashiCorp) в открытом бинаре их нет.
Итог: для новых проектов в 2026 OpenTofu выбор по умолчанию с меньшим риском:
идентичный синтаксис, та же экосистема провайдеров, плюс нативное шифрование
state и вендор-нейтральное управление. Если уже есть большая инвестиция в
Terraform/HCP и используются Stacks есть смысл остаться или мигрировать не спеша.`)
utils.Note("tofu — почти всегда совместимая замена terraform: 'alias terraform=tofu', большинство конфигураций и .tfstate работают без изменений")
utils.Section("Лучшие практики")
utils.Code(`Структура проекта:
infra/

View file

@ -0,0 +1,82 @@
package softmodules
import (
"rubric/utils"
)
var ForgejoModule = Module{
Key: "forgejo",
Aliases: []string{"fj", "git-forge"},
Title: "Forgejo — самохостинг git-форжа",
Description: "Форк Gitea: Docker-развёртывание, Angie, SSH на нестандартном порту",
Run: showForgejo,
}
func showForgejo() {
utils.Header("Soft — Forgejo")
utils.Section("Что это")
utils.Code(`Forgejo самохостимый git-форж, форк Gitea. Свой репозиторий кода
с веб-интерфейсом, issues, pull request'ами, вики и (опционально)
Forgejo Actions для CI/CD как собственный GitHub/GitLab на своём VDS.
Развёрнут на git.thebookofmagnus.ru (пользователь mroot),
за Angie как реверс-прокси, SSH-доступ на порту 222.`)
utils.Section("Развёртывание через Docker")
utils.Code(`# docker-compose.yml (упрощённо, с SQLite не нужен отдельный БД-сервер)
services:
forgejo:
image: codeberg.org/forgejo/forgejo:latest
environment:
- FORGEJO__database__DB_TYPE=sqlite3
- FORGEJO__server__DOMAIN=git.thebookofmagnus.ru
- FORGEJO__server__SSH_PORT=222
- FORGEJO__service__DISABLE_REGISTRATION=true
volumes:
- ./data:/data
ports:
- "127.0.0.1:3000:3000"
- "222:22"
restart: unless-stopped`)
utils.Section("Angie как реверс-прокси")
utils.Code(`server {
listen 443 ssl;
server_name git.thebookofmagnus.ru;
ssl_certificate /etc/letsencrypt/live/git.thebookofmagnus.ru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/git.thebookofmagnus.ru/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
# HTTP-блок для certbot (webroot-метод, --nginx с Angie не работает):
server {
listen 80;
server_name git.thebookofmagnus.ru;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
}`)
utils.Section("Частые проблемы при развёртывании")
utils.Println("• certbot: NXDOMAIN — DNS A-запись ещё не разошлась, подождать пропагации")
utils.Println("• certbot: 404 на /.well-known/acme-challenge/ — не хватает HTTP(80) server-блока в Angie для сабдомена")
utils.Println("• До готовности DNS/TLS — временный доступ через SSH-туннель: ssh -L 3000:127.0.0.1:3000 user@host")
utils.Println("• Предупреждение о смене SSH host key при первом git push — решается добавлением ключа в ssh-agent")
utils.Println("• Management → Users в новых версиях Forgejo переехал в раздел \"Identity & access\", а не отдельная вкладка")
utils.Section("Настройка")
utils.Command("FORGEJO__service__DISABLE_REGISTRATION=true", "отключить самостоятельную регистрацию")
utils.Command("FORGEJO__ui__APP_NAME", "название, отображаемое в заголовке вкладки браузера")
utils.Command("/data/gitea/public/assets/img/", "путь для замены логотипа и favicon (в примонтированном volume)")
utils.Note("CI/CD (Forgejo Actions) можно не настраивать сразу — вполне рабочий вариант отложить его на потом")
utils.Footer()
}

View file

@ -0,0 +1,75 @@
package softmodules
import (
"fmt"
"rubric/utils"
"strings"
)
// Module описывает один раздел справочника Soft
type Module struct {
Key string
Aliases []string
Title string
Description string
Run func()
}
// All — список всех модулей в порядке меню
var All = []Module{
ZolaModule,
ForgejoModule,
// сюда будут добавляться новые обзоры софта
}
// Find ищет модуль по ключу или алиасу
func Find(key string) (Module, bool) {
key = strings.ToLower(strings.TrimSpace(key))
for _, m := range All {
if m.Key == key {
return m, true
}
for _, a := range m.Aliases {
if a == key {
return m, true
}
}
}
return Module{}, false
}
// ListModules выводит краткий список модулей
func ListModules() {
fmt.Printf("\n%s%sРазделы справочника Soft:%s\n\n",
utils.ColorBold, utils.ColorCyan, utils.ColorReset)
for i, m := range All {
fmt.Printf(" %s%d.%s %-14s — %s\n",
utils.ColorDim, i+1, utils.ColorReset,
utils.ColorYellow+utils.ColorBold+m.Key+utils.ColorReset,
m.Title,
)
}
fmt.Println()
}
// Keywords возвращает ключевые слова для поиска по модулю
func Keywords(key string) []string {
kw := map[string][]string{
"zola": {
"zola", "статический сайт", "static site", "генератор сайта",
"SSG", "Rust", "шаблоны", "templates", "Tera", "markdown",
"сайт", "rsync", "деплой", "deploy", "front matter",
"content", "themes", "темы",
},
"forgejo": {
"forgejo", "git", "форж", "git forge", "gitea", "self-hosted",
"самохостинг", "репозиторий", "repository", "docker", "sqlite",
"SSH", "issues", "pull request", "CI", "actions", "webhook",
"git-сервер",
},
}
if v, ok := kw[key]; ok {
return v
}
return []string{}
}

View file

@ -0,0 +1,69 @@
package softmodules
import (
"rubric/utils"
)
var ZolaModule = Module{
Key: "zola",
Aliases: []string{"z", "static"},
Title: "Zola — генератор статических сайтов",
Description: "Однобинарный SSG на Rust: сборка, деплой через rsync, Angie",
Run: showZola,
}
func showZola() {
utils.Header("Soft — Zola")
utils.Section("Что это")
utils.Code(`Zola генератор статических сайтов (SSG), написанный на Rust.
Один бинарь без зависимостей, встроенный шаблонизатор Tera,
встроенный Sass-компилятор и Markdown-парсер ничего ставить
отдельно не нужно (в отличие от Jekyll/Hugo-плагинов и npm-тулчейнов).
Подходит для блогов, документации, персональных сайтов
как раз для thebookofmagnus.ru.`)
utils.Section("Основные команды")
utils.Command("zola init mysite", "создать новый проект сайта")
utils.Command("zola build", "собрать сайт в папку public/")
utils.Command("zola serve", "локальный dev-сервер с live reload")
utils.Command("zola check", "проверить ссылки и конфигурацию")
utils.Section("Модель развёртывания: локальная сборка + rsync")
utils.Code(`Zola не обязательно ставить на сервер она нужна только там,
где собирается сайт. Рабочая схема:
1. Пишешь контент локально
2. zola build получаешь папку public/
3. rsync -avz --delete public/ user@server:/var/www/mysite/public/
4. Angie отдаёт статику из /var/www/mysite/public
Разбор флагов rsync:
-a archive-режим: права, симлинки, время модификации
-v verbose видно что копируется
-z сжатие при передаче
--delete удаляет на сервере файлы, которых больше нет локально
(важно при переименовании/удалении страниц)
Альтернатива автосборка на сервере: bare git-репозиторий +
post-receive хук, который сам делает checkout + zola build +
копирование в /var/www/mysite/public. Проще для старта вариант
с локальной сборкой и rsync, без Zola на самом VDS.`)
utils.Section("Раздача через Angie")
utils.Code(`server {
listen 443 ssl;
server_name thebookofmagnus.ru www.thebookofmagnus.ru;
root /var/www/mysite/public;
index index.html;
ssl_certificate /etc/letsencrypt/live/thebookofmagnus.ru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/thebookofmagnus.ru/privkey.pem;
}`)
utils.Note("certbot с Angie: плагин --nginx конфликтует с Angie. Всегда используй --webroot (webroot-папка вроде /var/www/certbot + location /.well-known/acme-challenge/ в HTTP-server-блоке)")
utils.Footer()
}

BIN
rubric

Binary file not shown.