diff --git a/README.md b/README.md index 431373b..3cf09c5 100755 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ 1. go Справочник по языку Go Установка, синтаксис, типы, функции, горутины... + 5. soft Справочник по софту + Обзоры используемого софта: Zola, Forgejo... ``` --- diff --git a/catalog/catalog.go b/catalog/catalog.go index c9195c5..f249709 100755 --- a/catalog/catalog.go +++ b/catalog/catalog.go @@ -23,6 +23,7 @@ var All = []Book{ LinuxBook, DevOpsBook, InfoSecBook, + SoftBook, } // Find ищет книгу по ключу или алиасу diff --git a/catalog/search.go b/catalog/search.go index b53c252..383a081 100755 --- a/catalog/search.go +++ b/catalog/search.go @@ -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 } diff --git a/catalog/softbook.go b/catalog/softbook.go new file mode 100644 index 0000000..48015f3 --- /dev/null +++ b/catalog/softbook.go @@ -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) +} diff --git a/main.go b/main.go index 1f5aa1c..7273d6c 100755 --- a/main.go +++ b/main.go @@ -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) diff --git a/modules/devopsmodules/devops_registry.go b/modules/devopsmodules/devops_registry.go index 7fa4465..10452a5 100755 --- a/modules/devopsmodules/devops_registry.go +++ b/modules/devopsmodules/devops_registry.go @@ -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", diff --git a/modules/devopsmodules/devops_terraform.go b/modules/devopsmodules/devops_terraform.go index 80abdb4..4609d9d 100755 --- a/modules/devopsmodules/devops_terraform.go +++ b/modules/devopsmodules/devops_terraform.go @@ -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/ diff --git a/modules/softmodules/soft_forgejo.go b/modules/softmodules/soft_forgejo.go new file mode 100644 index 0000000..adac2b3 --- /dev/null +++ b/modules/softmodules/soft_forgejo.go @@ -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() +} diff --git a/modules/softmodules/soft_registry.go b/modules/softmodules/soft_registry.go new file mode 100644 index 0000000..68e59db --- /dev/null +++ b/modules/softmodules/soft_registry.go @@ -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{} +} diff --git a/modules/softmodules/soft_zola.go b/modules/softmodules/soft_zola.go new file mode 100644 index 0000000..b21fac0 --- /dev/null +++ b/modules/softmodules/soft_zola.go @@ -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() +} diff --git a/rubric b/rubric index 7bbdf8e..8b5b15f 100755 Binary files a/rubric and b/rubric differ